phasorpy 0.7__cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.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 +9 -0
- phasorpy/__main__.py +7 -0
- phasorpy/_phasorpy.cpython-312-aarch64-linux-gnu.so +0 -0
- phasorpy/_phasorpy.pyx +2688 -0
- phasorpy/_typing.py +77 -0
- phasorpy/_utils.py +786 -0
- phasorpy/cli.py +160 -0
- phasorpy/cluster.py +200 -0
- phasorpy/color.py +589 -0
- phasorpy/component.py +707 -0
- phasorpy/conftest.py +38 -0
- phasorpy/cursor.py +500 -0
- phasorpy/datasets.py +722 -0
- phasorpy/experimental.py +310 -0
- phasorpy/io/__init__.py +138 -0
- phasorpy/io/_flimlabs.py +360 -0
- phasorpy/io/_leica.py +331 -0
- phasorpy/io/_ometiff.py +444 -0
- phasorpy/io/_other.py +890 -0
- phasorpy/io/_simfcs.py +652 -0
- phasorpy/lifetime.py +2058 -0
- phasorpy/phasor.py +2018 -0
- phasorpy/plot/__init__.py +27 -0
- phasorpy/plot/_functions.py +723 -0
- phasorpy/plot/_lifetime_plots.py +563 -0
- phasorpy/plot/_phasorplot.py +1507 -0
- phasorpy/plot/_phasorplot_fret.py +561 -0
- phasorpy/py.typed +0 -0
- phasorpy/utils.py +172 -0
- phasorpy-0.7.dist-info/METADATA +74 -0
- phasorpy-0.7.dist-info/RECORD +36 -0
- phasorpy-0.7.dist-info/WHEEL +7 -0
- phasorpy-0.7.dist-info/entry_points.txt +2 -0
- phasorpy-0.7.dist-info/licenses/LICENSE.txt +21 -0
- phasorpy-0.7.dist-info/top_level.txt +1 -0
- phasorpy.libs/libgomp-947d5fa1.so.1.0.0 +0 -0
phasorpy/_phasorpy.pyx
ADDED
@@ -0,0 +1,2688 @@
|
|
1
|
+
# distutils: language = c
|
2
|
+
# cython: language_level = 3
|
3
|
+
# cython: boundscheck = False
|
4
|
+
# cython: wraparound = False
|
5
|
+
# cython: cdivision = True
|
6
|
+
# cython: nonecheck = False
|
7
|
+
# cython: freethreading_compatible = True
|
8
|
+
|
9
|
+
"""Cython implementation of low-level functions for the PhasorPy library."""
|
10
|
+
|
11
|
+
cimport cython
|
12
|
+
|
13
|
+
from cython.parallel import parallel, prange
|
14
|
+
|
15
|
+
from libc.math cimport (
|
16
|
+
INFINITY,
|
17
|
+
M_PI,
|
18
|
+
NAN,
|
19
|
+
atan,
|
20
|
+
atan2,
|
21
|
+
copysign,
|
22
|
+
cos,
|
23
|
+
exp,
|
24
|
+
fabs,
|
25
|
+
floor,
|
26
|
+
hypot,
|
27
|
+
isnan,
|
28
|
+
sin,
|
29
|
+
sqrt,
|
30
|
+
tan,
|
31
|
+
)
|
32
|
+
from libc.stdint cimport (
|
33
|
+
int8_t,
|
34
|
+
int16_t,
|
35
|
+
int32_t,
|
36
|
+
int64_t,
|
37
|
+
uint8_t,
|
38
|
+
uint16_t,
|
39
|
+
uint32_t,
|
40
|
+
uint64_t,
|
41
|
+
)
|
42
|
+
|
43
|
+
ctypedef fused float_t:
|
44
|
+
float
|
45
|
+
double
|
46
|
+
|
47
|
+
ctypedef fused uint_t:
|
48
|
+
uint8_t
|
49
|
+
uint16_t
|
50
|
+
uint32_t
|
51
|
+
uint64_t
|
52
|
+
|
53
|
+
ctypedef fused int_t:
|
54
|
+
int8_t
|
55
|
+
int16_t
|
56
|
+
int32_t
|
57
|
+
int64_t
|
58
|
+
|
59
|
+
ctypedef fused signal_t:
|
60
|
+
uint8_t
|
61
|
+
uint16_t
|
62
|
+
uint32_t
|
63
|
+
uint64_t
|
64
|
+
int8_t
|
65
|
+
int16_t
|
66
|
+
int32_t
|
67
|
+
int64_t
|
68
|
+
float
|
69
|
+
double
|
70
|
+
|
71
|
+
from libc.stdlib cimport free, malloc
|
72
|
+
|
73
|
+
|
74
|
+
def _phasor_from_signal(
|
75
|
+
float_t[:, :, ::1] phasor,
|
76
|
+
const signal_t[:, :, ::1] signal,
|
77
|
+
const double[:, :, ::1] sincos,
|
78
|
+
const bint normalize,
|
79
|
+
const int num_threads,
|
80
|
+
):
|
81
|
+
"""Return phasor coordinates from signal along middle axis.
|
82
|
+
|
83
|
+
Parameters
|
84
|
+
----------
|
85
|
+
phasor : 3D memoryview of float32 or float64
|
86
|
+
Writable buffer of three dimensions where calculated phasor
|
87
|
+
coordinates are stored:
|
88
|
+
|
89
|
+
0. mean, real, and imaginary components
|
90
|
+
1. lower dimensions flat
|
91
|
+
2. upper dimensions flat
|
92
|
+
|
93
|
+
signal : 3D memoryview of float32 or float64
|
94
|
+
Buffer of three dimensions containing signal:
|
95
|
+
|
96
|
+
0. lower dimensions flat
|
97
|
+
1. dimension over which to compute FFT, number samples
|
98
|
+
2. upper dimensions flat
|
99
|
+
|
100
|
+
sincos : 3D memoryview of float64
|
101
|
+
Buffer of three dimensions containing sine and cosine terms to be
|
102
|
+
multiplied with signal:
|
103
|
+
|
104
|
+
0. number harmonics
|
105
|
+
1. number samples
|
106
|
+
2. cos and sin
|
107
|
+
|
108
|
+
normalize : bool
|
109
|
+
Normalize phasor coordinates.
|
110
|
+
num_threads : int
|
111
|
+
Number of OpenMP threads to use for parallelization.
|
112
|
+
|
113
|
+
Notes
|
114
|
+
-----
|
115
|
+
This implementation requires contiguous input arrays.
|
116
|
+
|
117
|
+
"""
|
118
|
+
cdef:
|
119
|
+
float_t[:, ::1] mean
|
120
|
+
float_t[:, :, ::1] real, imag
|
121
|
+
ssize_t samples = signal.shape[1]
|
122
|
+
ssize_t harmonics = sincos.shape[0]
|
123
|
+
ssize_t i, j, k, h
|
124
|
+
double dc, re, im, sample
|
125
|
+
|
126
|
+
# TODO: use Numpy iterator API?
|
127
|
+
# https://numpy.org/devdocs/reference/c-api/iterator.html
|
128
|
+
|
129
|
+
if (
|
130
|
+
samples < 2
|
131
|
+
or harmonics > samples // 2
|
132
|
+
or phasor.shape[0] != harmonics * 2 + 1
|
133
|
+
or phasor.shape[1] != signal.shape[0]
|
134
|
+
or phasor.shape[2] != signal.shape[2]
|
135
|
+
):
|
136
|
+
raise ValueError('invalid shape of phasor or signal')
|
137
|
+
if sincos.shape[1] != samples or sincos.shape[2] != 2:
|
138
|
+
raise ValueError('invalid shape of sincos')
|
139
|
+
|
140
|
+
mean = phasor[0]
|
141
|
+
real = phasor[1 : 1 + harmonics]
|
142
|
+
imag = phasor[1 + harmonics : 1 + harmonics * 2]
|
143
|
+
|
144
|
+
if num_threads > 1 and signal.shape[0] >= num_threads:
|
145
|
+
# parallelize outer dimensions
|
146
|
+
with nogil, parallel(num_threads=num_threads):
|
147
|
+
for i in prange(signal.shape[0]):
|
148
|
+
for h in range(harmonics):
|
149
|
+
for j in range(signal.shape[2]):
|
150
|
+
dc = 0.0
|
151
|
+
re = 0.0
|
152
|
+
im = 0.0
|
153
|
+
for k in range(samples):
|
154
|
+
sample = <double> signal[i, k, j]
|
155
|
+
dc = dc + sample
|
156
|
+
re = re + sample * sincos[h, k, 0]
|
157
|
+
im = im + sample * sincos[h, k, 1]
|
158
|
+
if normalize:
|
159
|
+
if dc != 0.0:
|
160
|
+
# includes isnan(dc)
|
161
|
+
re = re / dc
|
162
|
+
im = im / dc
|
163
|
+
dc = dc / samples
|
164
|
+
else:
|
165
|
+
# dc = 0.0
|
166
|
+
re = NAN if re == 0.0 else re * INFINITY
|
167
|
+
im = NAN if im == 0.0 else im * INFINITY
|
168
|
+
if h == 0:
|
169
|
+
mean[i, j] = <float_t> dc
|
170
|
+
real[h, i, j] = <float_t> re
|
171
|
+
imag[h, i, j] = <float_t> im
|
172
|
+
|
173
|
+
elif num_threads > 1 and signal.shape[2] >= num_threads:
|
174
|
+
# parallelize inner dimensions
|
175
|
+
# TODO: do not use when not built with OpenMP
|
176
|
+
with nogil, parallel(num_threads=num_threads):
|
177
|
+
for j in prange(signal.shape[2]):
|
178
|
+
for h in range(harmonics):
|
179
|
+
for i in range(signal.shape[0]):
|
180
|
+
dc = 0.0
|
181
|
+
re = 0.0
|
182
|
+
im = 0.0
|
183
|
+
for k in range(samples):
|
184
|
+
sample = <double> signal[i, k, j]
|
185
|
+
dc = dc + sample
|
186
|
+
re = re + sample * sincos[h, k, 0]
|
187
|
+
im = im + sample * sincos[h, k, 1]
|
188
|
+
if normalize:
|
189
|
+
if dc != 0.0:
|
190
|
+
# includes isnan(dc)
|
191
|
+
re = re / dc
|
192
|
+
im = im / dc
|
193
|
+
dc = dc / samples
|
194
|
+
else:
|
195
|
+
# dc = 0.0
|
196
|
+
re = NAN if re == 0.0 else re * INFINITY
|
197
|
+
im = NAN if im == 0.0 else im * INFINITY
|
198
|
+
if h == 0:
|
199
|
+
mean[i, j] = <float_t> dc
|
200
|
+
real[h, i, j] = <float_t> re
|
201
|
+
imag[h, i, j] = <float_t> im
|
202
|
+
|
203
|
+
else:
|
204
|
+
# do not parallelize
|
205
|
+
with nogil:
|
206
|
+
for h in range(harmonics):
|
207
|
+
# TODO: move harmonics to an inner loop?
|
208
|
+
for i in range(signal.shape[0]):
|
209
|
+
for j in range(signal.shape[2]):
|
210
|
+
dc = 0.0
|
211
|
+
re = 0.0
|
212
|
+
im = 0.0
|
213
|
+
for k in range(samples):
|
214
|
+
sample = <double> signal[i, k, j]
|
215
|
+
dc += sample
|
216
|
+
re += sample * sincos[h, k, 0]
|
217
|
+
im += sample * sincos[h, k, 1]
|
218
|
+
if normalize:
|
219
|
+
if dc != 0.0:
|
220
|
+
# includes isnan(dc)
|
221
|
+
re /= dc
|
222
|
+
im /= dc
|
223
|
+
dc = dc / samples
|
224
|
+
else:
|
225
|
+
# dc = 0.0
|
226
|
+
re = NAN if re == 0.0 else re * INFINITY
|
227
|
+
im = NAN if im == 0.0 else im * INFINITY
|
228
|
+
if h == 0:
|
229
|
+
mean[i, j] = <float_t> dc
|
230
|
+
real[h, i, j] = <float_t> re
|
231
|
+
imag[h, i, j] = <float_t> im
|
232
|
+
|
233
|
+
|
234
|
+
def _phasor_from_lifetime(
|
235
|
+
float_t[:, :, ::1] phasor,
|
236
|
+
const double[::1] frequency,
|
237
|
+
const double[:, ::1] lifetime,
|
238
|
+
const double[:, ::1] fraction,
|
239
|
+
const double unit_conversion,
|
240
|
+
const bint preexponential,
|
241
|
+
):
|
242
|
+
"""Calculate phasor coordinates from lifetime components.
|
243
|
+
|
244
|
+
Parameters
|
245
|
+
----------
|
246
|
+
phasor : 3D memoryview of float32 or float64
|
247
|
+
Writable buffer of three dimensions where calculated phasor
|
248
|
+
coordinates are stored:
|
249
|
+
|
250
|
+
0. real and imaginary components
|
251
|
+
1. frequencies
|
252
|
+
2. lifetimes or fractions
|
253
|
+
|
254
|
+
frequency : 2D memoryview of float64
|
255
|
+
One-dimensional sequence of laser-pulse or modulation frequencies.
|
256
|
+
lifetime : 2D memoryview of float64
|
257
|
+
Buffer of two dimensions:
|
258
|
+
|
259
|
+
0. lifetimes
|
260
|
+
1. components of lifetimes
|
261
|
+
|
262
|
+
fraction : 2D memoryview of float64
|
263
|
+
Buffer of two dimensions:
|
264
|
+
|
265
|
+
0. fractions
|
266
|
+
1. fractions of lifetime components
|
267
|
+
|
268
|
+
unit_conversion : float
|
269
|
+
Product of `frequency` and `lifetime` units' prefix factors.
|
270
|
+
1e-3 for MHz and ns. 1.0 for Hz and s.
|
271
|
+
preexponential : bool
|
272
|
+
If true, fractions are pre-exponential amplitudes, else fractional
|
273
|
+
intensities.
|
274
|
+
|
275
|
+
"""
|
276
|
+
cdef:
|
277
|
+
ssize_t nfreq = frequency.shape[0] # number frequencies
|
278
|
+
ssize_t ncomp = lifetime.shape[1] # number lifetime components
|
279
|
+
ssize_t ntau = lifetime.shape[0] # number lifetimes
|
280
|
+
ssize_t nfrac = fraction.shape[0] # number fractions
|
281
|
+
double twopi = 2.0 * M_PI * unit_conversion
|
282
|
+
double freq, tau, frac, sum, re, im, gs
|
283
|
+
ssize_t f, t, s
|
284
|
+
|
285
|
+
if phasor.shape[0] != 2 or phasor.shape[1] != nfreq:
|
286
|
+
raise ValueError(
|
287
|
+
f'invalid {phasor.shape=!r} != (2, {nfreq}, -1))'
|
288
|
+
)
|
289
|
+
if fraction.shape[1] != ncomp:
|
290
|
+
raise ValueError(f'{lifetime.shape[1]=} != {fraction.shape[1]=}')
|
291
|
+
|
292
|
+
if nfreq == 1 and ntau == 1 and nfrac == 1 and ncomp == 1:
|
293
|
+
# scalar
|
294
|
+
tau = lifetime[0, 0] * frequency[0] * twopi # omega_tau
|
295
|
+
gs = 1.0 / (1.0 + tau * tau)
|
296
|
+
phasor[0, 0, 0] = <float_t> gs
|
297
|
+
phasor[1, 0, 0] = <float_t> (gs * tau)
|
298
|
+
return
|
299
|
+
|
300
|
+
if ntau == nfrac:
|
301
|
+
# fractions specified for all lifetime components
|
302
|
+
if phasor.shape[2] != ntau:
|
303
|
+
raise ValueError(f'{phasor.shape[2]=} != {ntau}')
|
304
|
+
with nogil:
|
305
|
+
for f in range(nfreq):
|
306
|
+
freq = frequency[f] * twopi # omega
|
307
|
+
for t in range(ntau):
|
308
|
+
re = 0.0
|
309
|
+
im = 0.0
|
310
|
+
sum = 0.0
|
311
|
+
if preexponential:
|
312
|
+
for s in range(ncomp):
|
313
|
+
sum += fraction[t, s] * lifetime[t, s] # Fdc
|
314
|
+
else:
|
315
|
+
for s in range(ncomp):
|
316
|
+
sum += fraction[t, s]
|
317
|
+
if fabs(sum) < 1e-15:
|
318
|
+
phasor[0, f, t] = <float_t> NAN
|
319
|
+
phasor[1, f, t] = <float_t> NAN
|
320
|
+
continue
|
321
|
+
for s in range(ncomp):
|
322
|
+
tau = lifetime[t, s]
|
323
|
+
frac = fraction[t, s] / sum
|
324
|
+
if preexponential:
|
325
|
+
frac *= tau
|
326
|
+
tau *= freq # omega_tau
|
327
|
+
gs = frac / (1.0 + tau * tau)
|
328
|
+
re += gs
|
329
|
+
im += gs * tau
|
330
|
+
phasor[0, f, t] = <float_t> re
|
331
|
+
phasor[1, f, t] = <float_t> im
|
332
|
+
return
|
333
|
+
|
334
|
+
if ntau > 1 and nfrac == 1:
|
335
|
+
# varying lifetime components, same fractions
|
336
|
+
if phasor.shape[2] != ntau:
|
337
|
+
raise ValueError(f'{phasor.shape[2]=} != {ntau}')
|
338
|
+
with nogil:
|
339
|
+
for f in range(nfreq):
|
340
|
+
freq = frequency[f] * twopi # omega
|
341
|
+
sum = 0.0
|
342
|
+
if not preexponential:
|
343
|
+
for s in range(ncomp):
|
344
|
+
sum += fraction[0, s]
|
345
|
+
for t in range(ntau):
|
346
|
+
if preexponential:
|
347
|
+
sum = 0.0
|
348
|
+
for s in range(ncomp):
|
349
|
+
sum += fraction[0, s] * lifetime[t, s] # Fdc
|
350
|
+
if fabs(sum) < 1e-15:
|
351
|
+
phasor[0, f, t] = <float_t> NAN
|
352
|
+
phasor[1, f, t] = <float_t> NAN
|
353
|
+
continue
|
354
|
+
re = 0.0
|
355
|
+
im = 0.0
|
356
|
+
for s in range(ncomp):
|
357
|
+
tau = lifetime[t, s]
|
358
|
+
frac = fraction[0, s] / sum
|
359
|
+
if preexponential:
|
360
|
+
frac *= tau
|
361
|
+
tau *= freq # omega_tau
|
362
|
+
gs = frac / (1.0 + tau * tau)
|
363
|
+
re += gs
|
364
|
+
im += gs * tau
|
365
|
+
phasor[0, f, t] = <float_t> re
|
366
|
+
phasor[1, f, t] = <float_t> im
|
367
|
+
return
|
368
|
+
|
369
|
+
if ntau == 1 and nfrac > 1:
|
370
|
+
# same lifetime components, varying fractions
|
371
|
+
if phasor.shape[2] != nfrac:
|
372
|
+
raise ValueError(f'{phasor.shape[2]=} != {nfrac}')
|
373
|
+
with nogil:
|
374
|
+
for f in range(nfreq):
|
375
|
+
freq = frequency[f] * twopi # omega
|
376
|
+
for t in range(nfrac):
|
377
|
+
re = 0.0
|
378
|
+
im = 0.0
|
379
|
+
sum = 0.0
|
380
|
+
if preexponential:
|
381
|
+
for s in range(ncomp):
|
382
|
+
sum += fraction[t, s] * lifetime[0, s] # Fdc
|
383
|
+
else:
|
384
|
+
for s in range(ncomp):
|
385
|
+
sum += fraction[t, s]
|
386
|
+
if fabs(sum) < 1e-15:
|
387
|
+
phasor[0, f, t] = <float_t> NAN
|
388
|
+
phasor[1, f, t] = <float_t> NAN
|
389
|
+
continue
|
390
|
+
for s in range(ncomp):
|
391
|
+
tau = lifetime[0, s]
|
392
|
+
frac = fraction[t, s] / sum
|
393
|
+
if preexponential:
|
394
|
+
frac *= tau
|
395
|
+
tau *= freq # omega_tau
|
396
|
+
gs = frac / (1.0 + tau * tau)
|
397
|
+
re += gs
|
398
|
+
im += gs * tau
|
399
|
+
phasor[0, f, t] = <float_t> re
|
400
|
+
phasor[1, f, t] = <float_t> im
|
401
|
+
return
|
402
|
+
|
403
|
+
raise ValueError(
|
404
|
+
f'{lifetime.shape[0]=} and {fraction.shape[0]=} do not match'
|
405
|
+
)
|
406
|
+
|
407
|
+
|
408
|
+
def _gaussian_signal(
|
409
|
+
float_t[::1] signal,
|
410
|
+
const double mean,
|
411
|
+
const double stdev,
|
412
|
+
):
|
413
|
+
"""Return normal distribution, wrapped around at borders.
|
414
|
+
|
415
|
+
Parameters
|
416
|
+
----------
|
417
|
+
signal : memoryview of float32 or float64
|
418
|
+
Writable buffer where calculated signal samples are stored.
|
419
|
+
mean : float
|
420
|
+
Mean of normal distribution.
|
421
|
+
stdev : float
|
422
|
+
Standard deviation of normal distribution.
|
423
|
+
|
424
|
+
"""
|
425
|
+
cdef:
|
426
|
+
ssize_t samples = signal.shape[0]
|
427
|
+
ssize_t folds = 1 # TODO: calculate from stddev and samples
|
428
|
+
ssize_t i
|
429
|
+
double t, c
|
430
|
+
|
431
|
+
if stdev <= 0.0 or samples < 1:
|
432
|
+
return
|
433
|
+
|
434
|
+
with nogil:
|
435
|
+
c = 1.0 / sqrt(2.0 * M_PI) * stdev
|
436
|
+
|
437
|
+
for i in range(-folds * samples, (folds + 1) * samples):
|
438
|
+
t = (<double> i - mean) / stdev
|
439
|
+
t *= t
|
440
|
+
t = c * exp(-t / 2.0)
|
441
|
+
# i %= samples
|
442
|
+
i -= samples * <ssize_t> floor(<double> i / samples)
|
443
|
+
signal[i] += <float_t> t
|
444
|
+
|
445
|
+
|
446
|
+
###############################################################################
|
447
|
+
# FRET model
|
448
|
+
|
449
|
+
|
450
|
+
@cython.ufunc
|
451
|
+
cdef (double, double) _phasor_from_fret_donor(
|
452
|
+
double omega,
|
453
|
+
double donor_lifetime,
|
454
|
+
double fret_efficiency,
|
455
|
+
double donor_fretting,
|
456
|
+
double donor_background,
|
457
|
+
double background_real,
|
458
|
+
double background_imag,
|
459
|
+
) noexcept nogil:
|
460
|
+
"""Return phasor coordinates of FRET donor channel.
|
461
|
+
|
462
|
+
See :py:func:`phasor_from_fret_donor` for parameter definitions.
|
463
|
+
|
464
|
+
"""
|
465
|
+
cdef:
|
466
|
+
double real, imag
|
467
|
+
double quenched_real, quenched_imag # quenched donor
|
468
|
+
double f_pure, f_quenched, sum
|
469
|
+
|
470
|
+
if fret_efficiency < 0.0:
|
471
|
+
fret_efficiency = 0.0
|
472
|
+
elif fret_efficiency > 1.0:
|
473
|
+
fret_efficiency = 1.0
|
474
|
+
|
475
|
+
if donor_fretting < 0.0:
|
476
|
+
donor_fretting = 0.0
|
477
|
+
elif donor_fretting > 1.0:
|
478
|
+
donor_fretting = 1.0
|
479
|
+
|
480
|
+
if donor_background < 0.0:
|
481
|
+
donor_background = 0.0
|
482
|
+
|
483
|
+
f_pure = 1.0 - donor_fretting
|
484
|
+
f_quenched = (1.0 - fret_efficiency) * donor_fretting
|
485
|
+
sum = f_pure + f_quenched + donor_background
|
486
|
+
if sum < 1e-9:
|
487
|
+
# no signal in donor channel
|
488
|
+
return 1.0, 0.0
|
489
|
+
|
490
|
+
# phasor of pure donor at frequency
|
491
|
+
real, imag = phasor_from_single_lifetime(donor_lifetime, omega)
|
492
|
+
|
493
|
+
# phasor of quenched donor
|
494
|
+
quenched_real, quenched_imag = phasor_from_single_lifetime(
|
495
|
+
donor_lifetime * (1.0 - fret_efficiency), omega
|
496
|
+
)
|
497
|
+
|
498
|
+
# weighted average
|
499
|
+
real = (
|
500
|
+
real * f_pure
|
501
|
+
+ quenched_real * f_quenched
|
502
|
+
+ donor_background * background_real
|
503
|
+
) / sum
|
504
|
+
|
505
|
+
imag = (
|
506
|
+
imag * f_pure
|
507
|
+
+ quenched_imag * f_quenched
|
508
|
+
+ background_imag * donor_background
|
509
|
+
) / sum
|
510
|
+
|
511
|
+
return real, imag
|
512
|
+
|
513
|
+
|
514
|
+
@cython.ufunc
|
515
|
+
cdef (double, double) _phasor_from_fret_acceptor(
|
516
|
+
double omega,
|
517
|
+
double donor_lifetime,
|
518
|
+
double acceptor_lifetime,
|
519
|
+
double fret_efficiency,
|
520
|
+
double donor_fretting,
|
521
|
+
double donor_bleedthrough,
|
522
|
+
double acceptor_bleedthrough,
|
523
|
+
double acceptor_background,
|
524
|
+
double background_real,
|
525
|
+
double background_imag,
|
526
|
+
) noexcept nogil:
|
527
|
+
"""Return phasor coordinates of FRET acceptor channel.
|
528
|
+
|
529
|
+
See :py:func:`phasor_from_fret_acceptor` for parameter definitions.
|
530
|
+
|
531
|
+
"""
|
532
|
+
cdef:
|
533
|
+
double phi, mod
|
534
|
+
double donor_real, donor_imag
|
535
|
+
double acceptor_real, acceptor_imag
|
536
|
+
double quenched_real, quenched_imag # quenched donor
|
537
|
+
double sensitized_real, sensitized_imag # sensitized acceptor
|
538
|
+
double sum, f_donor, f_acceptor
|
539
|
+
|
540
|
+
if fret_efficiency < 0.0:
|
541
|
+
fret_efficiency = 0.0
|
542
|
+
elif fret_efficiency > 1.0:
|
543
|
+
fret_efficiency = 1.0
|
544
|
+
|
545
|
+
if donor_fretting < 0.0:
|
546
|
+
donor_fretting = 0.0
|
547
|
+
elif donor_fretting > 1.0:
|
548
|
+
donor_fretting = 1.0
|
549
|
+
|
550
|
+
if donor_bleedthrough < 0.0:
|
551
|
+
donor_bleedthrough = 0.0
|
552
|
+
if acceptor_bleedthrough < 0.0:
|
553
|
+
acceptor_bleedthrough = 0.0
|
554
|
+
if acceptor_background < 0.0:
|
555
|
+
acceptor_background = 0.0
|
556
|
+
|
557
|
+
# phasor of pure donor at frequency
|
558
|
+
donor_real, donor_imag = phasor_from_single_lifetime(donor_lifetime, omega)
|
559
|
+
|
560
|
+
if fret_efficiency == 0.0:
|
561
|
+
quenched_real = donor_real
|
562
|
+
quenched_imag = donor_imag
|
563
|
+
else:
|
564
|
+
# phasor of quenched donor
|
565
|
+
quenched_real, quenched_imag = phasor_from_single_lifetime(
|
566
|
+
donor_lifetime * (1.0 - fret_efficiency), omega
|
567
|
+
)
|
568
|
+
|
569
|
+
# phasor of pure and quenched donor
|
570
|
+
donor_real, donor_imag = linear_combination(
|
571
|
+
1.0,
|
572
|
+
0.0,
|
573
|
+
donor_real,
|
574
|
+
donor_imag,
|
575
|
+
quenched_real,
|
576
|
+
quenched_imag,
|
577
|
+
1.0,
|
578
|
+
1.0 - fret_efficiency,
|
579
|
+
1.0 - donor_fretting
|
580
|
+
)
|
581
|
+
|
582
|
+
# phasor of acceptor at frequency
|
583
|
+
acceptor_real, acceptor_imag = phasor_from_single_lifetime(
|
584
|
+
acceptor_lifetime, omega
|
585
|
+
)
|
586
|
+
|
587
|
+
# phasor of acceptor sensitized by quenched donor
|
588
|
+
# TODO: use rotation formula
|
589
|
+
phi = (
|
590
|
+
atan2(quenched_imag, quenched_real)
|
591
|
+
+ atan2(acceptor_imag, acceptor_real)
|
592
|
+
)
|
593
|
+
mod = (
|
594
|
+
hypot(quenched_real, quenched_imag)
|
595
|
+
* hypot(acceptor_real, acceptor_imag)
|
596
|
+
)
|
597
|
+
sensitized_real = mod * cos(phi)
|
598
|
+
sensitized_imag = mod * sin(phi)
|
599
|
+
|
600
|
+
# weighted average
|
601
|
+
f_donor = donor_bleedthrough * (1.0 - donor_fretting * fret_efficiency)
|
602
|
+
f_acceptor = donor_fretting * fret_efficiency
|
603
|
+
sum = f_donor + f_acceptor + acceptor_bleedthrough + acceptor_background
|
604
|
+
if sum < 1e-9:
|
605
|
+
# no signal in acceptor channel
|
606
|
+
# do not return 0, 0 to avoid discontinuities
|
607
|
+
return sensitized_real, sensitized_imag
|
608
|
+
|
609
|
+
acceptor_real = (
|
610
|
+
donor_real * f_donor
|
611
|
+
+ sensitized_real * f_acceptor
|
612
|
+
+ acceptor_real * acceptor_bleedthrough
|
613
|
+
+ background_real * acceptor_background
|
614
|
+
) / sum
|
615
|
+
|
616
|
+
acceptor_imag = (
|
617
|
+
donor_imag * f_donor
|
618
|
+
+ sensitized_imag * f_acceptor
|
619
|
+
+ acceptor_imag * acceptor_bleedthrough
|
620
|
+
+ background_imag * acceptor_background
|
621
|
+
) / sum
|
622
|
+
|
623
|
+
return acceptor_real, acceptor_imag
|
624
|
+
|
625
|
+
|
626
|
+
cdef inline (double, double) linear_combination(
|
627
|
+
const double real,
|
628
|
+
const double imag,
|
629
|
+
const double real1,
|
630
|
+
const double imag1,
|
631
|
+
const double real2,
|
632
|
+
const double imag2,
|
633
|
+
double int1,
|
634
|
+
double int2,
|
635
|
+
double frac,
|
636
|
+
) noexcept nogil:
|
637
|
+
"""Return linear combinations of phasor coordinates."""
|
638
|
+
int1 *= frac
|
639
|
+
int2 *= 1.0 - frac
|
640
|
+
frac = int1 + int2
|
641
|
+
if fabs(frac) < 1e-15:
|
642
|
+
return real, imag
|
643
|
+
return (
|
644
|
+
(int1 * real1 + int2 * real2) / frac,
|
645
|
+
(int1 * imag1 + int2 * imag2) / frac
|
646
|
+
)
|
647
|
+
|
648
|
+
|
649
|
+
cdef inline (double, double) phasor_from_single_lifetime(
|
650
|
+
const double lifetime,
|
651
|
+
const double omega,
|
652
|
+
) noexcept nogil:
|
653
|
+
"""Return phasor coordinates from single lifetime component."""
|
654
|
+
cdef:
|
655
|
+
double t = omega * lifetime
|
656
|
+
double mod = 1.0 / sqrt(1.0 + t * t)
|
657
|
+
double phi = atan(t)
|
658
|
+
|
659
|
+
return mod * cos(phi), mod * sin(phi)
|
660
|
+
|
661
|
+
|
662
|
+
###############################################################################
|
663
|
+
# Phasor conversions
|
664
|
+
|
665
|
+
|
666
|
+
@cython.ufunc
|
667
|
+
cdef (float_t, float_t) _phasor_transform(
|
668
|
+
float_t real,
|
669
|
+
float_t imag,
|
670
|
+
float_t angle,
|
671
|
+
float_t scale,
|
672
|
+
) noexcept nogil:
|
673
|
+
"""Return rotated and scaled phasor coordinates."""
|
674
|
+
cdef:
|
675
|
+
double g, s
|
676
|
+
|
677
|
+
if isnan(real) or isnan(imag) or isnan(angle) or isnan(scale):
|
678
|
+
return <float_t> NAN, <float_t> NAN
|
679
|
+
|
680
|
+
g = scale * cos(angle)
|
681
|
+
s = scale * sin(angle)
|
682
|
+
|
683
|
+
return <float_t> (real * g - imag * s), <float_t> (real * s + imag * g)
|
684
|
+
|
685
|
+
|
686
|
+
@cython.ufunc
|
687
|
+
cdef (float_t, float_t) _phasor_transform_const(
|
688
|
+
float_t real,
|
689
|
+
float_t imag,
|
690
|
+
float_t real2,
|
691
|
+
float_t imag2,
|
692
|
+
) noexcept nogil:
|
693
|
+
"""Return rotated and scaled phasor coordinates."""
|
694
|
+
if isnan(real) or isnan(imag) or isnan(real2) or isnan(imag2):
|
695
|
+
return <float_t> NAN, <float_t> NAN
|
696
|
+
|
697
|
+
return real * real2 - imag * imag2, real * imag2 + imag * real2
|
698
|
+
|
699
|
+
|
700
|
+
@cython.ufunc
|
701
|
+
cdef (float_t, float_t) _phasor_to_polar(
|
702
|
+
float_t real,
|
703
|
+
float_t imag,
|
704
|
+
) noexcept nogil:
|
705
|
+
"""Return polar from phasor coordinates."""
|
706
|
+
if isnan(real) or isnan(imag):
|
707
|
+
return <float_t> NAN, <float_t> NAN
|
708
|
+
|
709
|
+
return (
|
710
|
+
<float_t> atan2(imag, real),
|
711
|
+
<float_t> sqrt(real * real + imag * imag)
|
712
|
+
)
|
713
|
+
|
714
|
+
|
715
|
+
@cython.ufunc
|
716
|
+
cdef (float_t, float_t) _phasor_from_polar(
|
717
|
+
float_t phase,
|
718
|
+
float_t modulation,
|
719
|
+
) noexcept nogil:
|
720
|
+
"""Return phasor from polar coordinates."""
|
721
|
+
if isnan(phase) or isnan(modulation):
|
722
|
+
return <float_t> NAN, <float_t> NAN
|
723
|
+
|
724
|
+
return (
|
725
|
+
modulation * <float_t> cos(phase),
|
726
|
+
modulation * <float_t> sin(phase)
|
727
|
+
)
|
728
|
+
|
729
|
+
|
730
|
+
@cython.ufunc
|
731
|
+
cdef (float_t, float_t) _phasor_to_apparent_lifetime(
|
732
|
+
float_t real,
|
733
|
+
float_t imag,
|
734
|
+
float_t omega,
|
735
|
+
) noexcept nogil:
|
736
|
+
"""Return apparent single lifetimes from phasor coordinates."""
|
737
|
+
cdef:
|
738
|
+
double tauphi = INFINITY
|
739
|
+
double taumod = INFINITY
|
740
|
+
double t
|
741
|
+
|
742
|
+
if isnan(real) or isnan(imag):
|
743
|
+
return <float_t> NAN, <float_t> NAN
|
744
|
+
|
745
|
+
t = real * real + imag * imag
|
746
|
+
if omega > 0.0 and t > 0.0:
|
747
|
+
if fabs(real * omega) > 0.0:
|
748
|
+
tauphi = imag / (real * omega)
|
749
|
+
if t <= 1.0:
|
750
|
+
taumod = sqrt(1.0 / t - 1.0) / omega
|
751
|
+
else:
|
752
|
+
taumod = 0.0
|
753
|
+
|
754
|
+
return <float_t> tauphi, <float_t> taumod
|
755
|
+
|
756
|
+
|
757
|
+
@cython.ufunc
|
758
|
+
cdef (float_t, float_t) _phasor_from_apparent_lifetime(
|
759
|
+
float_t tauphi,
|
760
|
+
float_t taumod,
|
761
|
+
float_t omega,
|
762
|
+
) noexcept nogil:
|
763
|
+
"""Return phasor coordinates from apparent single lifetimes."""
|
764
|
+
cdef:
|
765
|
+
double phi, mod, t
|
766
|
+
|
767
|
+
if isnan(tauphi) or isnan(taumod):
|
768
|
+
return <float_t> NAN, <float_t> NAN
|
769
|
+
|
770
|
+
t = omega * taumod
|
771
|
+
mod = 1.0 / sqrt(1.0 + t * t)
|
772
|
+
phi = atan(omega * tauphi)
|
773
|
+
return <float_t> (mod * cos(phi)), <float_t> (mod * sin(phi))
|
774
|
+
|
775
|
+
|
776
|
+
@cython.ufunc
|
777
|
+
cdef float_t _phasor_to_normal_lifetime(
|
778
|
+
float_t real,
|
779
|
+
float_t imag,
|
780
|
+
float_t omega,
|
781
|
+
) noexcept nogil:
|
782
|
+
"""Return normal lifetimes from phasor coordinates."""
|
783
|
+
cdef:
|
784
|
+
double taunorm = INFINITY
|
785
|
+
double t
|
786
|
+
|
787
|
+
if isnan(real) or isnan(imag):
|
788
|
+
return <float_t> NAN
|
789
|
+
|
790
|
+
omega *= omega
|
791
|
+
if omega > 0.0:
|
792
|
+
t = 0.5 * (1.0 + cos(atan2(imag, real - 0.5)))
|
793
|
+
if t <= 0.0:
|
794
|
+
taunorm = INFINITY
|
795
|
+
elif t > 1.0:
|
796
|
+
taunorm = NAN
|
797
|
+
else:
|
798
|
+
taunorm = sqrt((1.0 - t) / (omega * t))
|
799
|
+
|
800
|
+
return <float_t> taunorm
|
801
|
+
|
802
|
+
|
803
|
+
@cython.ufunc
|
804
|
+
cdef (float_t, float_t) _phasor_from_single_lifetime(
|
805
|
+
float_t lifetime,
|
806
|
+
float_t omega,
|
807
|
+
) noexcept nogil:
|
808
|
+
"""Return phasor coordinates from single lifetime component."""
|
809
|
+
cdef:
|
810
|
+
double phi, mod, t
|
811
|
+
|
812
|
+
if isnan(lifetime):
|
813
|
+
return <float_t> NAN, <float_t> NAN
|
814
|
+
|
815
|
+
t = omega * lifetime
|
816
|
+
phi = atan(t)
|
817
|
+
mod = 1.0 / sqrt(1.0 + t * t)
|
818
|
+
return <float_t> (mod * cos(phi)), <float_t> (mod * sin(phi))
|
819
|
+
|
820
|
+
|
821
|
+
@cython.ufunc
|
822
|
+
cdef (float_t, float_t) _polar_from_single_lifetime(
|
823
|
+
float_t lifetime,
|
824
|
+
float_t omega,
|
825
|
+
) noexcept nogil:
|
826
|
+
"""Return polar coordinates from single lifetime component."""
|
827
|
+
cdef:
|
828
|
+
double t
|
829
|
+
|
830
|
+
if isnan(lifetime):
|
831
|
+
return <float_t> NAN, <float_t> NAN
|
832
|
+
|
833
|
+
t = omega * lifetime
|
834
|
+
return <float_t> atan(t), <float_t> (1.0 / sqrt(1.0 + t * t))
|
835
|
+
|
836
|
+
|
837
|
+
@cython.ufunc
|
838
|
+
cdef (float_t, float_t) _polar_to_apparent_lifetime(
|
839
|
+
float_t phase,
|
840
|
+
float_t modulation,
|
841
|
+
float_t omega,
|
842
|
+
) noexcept nogil:
|
843
|
+
"""Return apparent single lifetimes from polar coordinates."""
|
844
|
+
cdef:
|
845
|
+
double tauphi = INFINITY
|
846
|
+
double taumod = INFINITY
|
847
|
+
double t
|
848
|
+
|
849
|
+
if isnan(phase) or isnan(modulation):
|
850
|
+
return <float_t> NAN, <float_t> NAN
|
851
|
+
|
852
|
+
t = modulation * modulation
|
853
|
+
if omega > 0.0 and t > 0.0:
|
854
|
+
tauphi = tan(phase) / omega
|
855
|
+
if t <= 1.0:
|
856
|
+
taumod = sqrt(1.0 / t - 1.0) / omega
|
857
|
+
else:
|
858
|
+
taumod = 0.0
|
859
|
+
return <float_t> tauphi, <float_t> taumod
|
860
|
+
|
861
|
+
|
862
|
+
@cython.ufunc
|
863
|
+
cdef (float_t, float_t) _polar_from_apparent_lifetime(
|
864
|
+
float_t tauphi,
|
865
|
+
float_t taumod,
|
866
|
+
float_t omega,
|
867
|
+
) noexcept nogil:
|
868
|
+
"""Return polar coordinates from apparent single lifetimes."""
|
869
|
+
cdef:
|
870
|
+
double t
|
871
|
+
|
872
|
+
if isnan(tauphi) or isnan(taumod):
|
873
|
+
return <float_t> NAN, <float_t> NAN
|
874
|
+
|
875
|
+
t = omega * taumod
|
876
|
+
return (
|
877
|
+
<float_t> (atan(omega * tauphi)),
|
878
|
+
<float_t> (1.0 / sqrt(1.0 + t * t))
|
879
|
+
)
|
880
|
+
|
881
|
+
|
882
|
+
@cython.ufunc
|
883
|
+
cdef (float_t, float_t) _polar_from_reference(
|
884
|
+
float_t measured_phase,
|
885
|
+
float_t measured_modulation,
|
886
|
+
float_t known_phase,
|
887
|
+
float_t known_modulation,
|
888
|
+
) noexcept nogil:
|
889
|
+
"""Return polar coordinates for calibration from reference coordinates."""
|
890
|
+
if (
|
891
|
+
isnan(measured_phase)
|
892
|
+
or isnan(measured_modulation)
|
893
|
+
or isnan(known_phase)
|
894
|
+
or isnan(known_modulation)
|
895
|
+
):
|
896
|
+
return <float_t> NAN, <float_t> NAN
|
897
|
+
|
898
|
+
if fabs(measured_modulation) == 0.0:
|
899
|
+
# return known_phase - measured_phase, <float_t> INFINITY
|
900
|
+
return (
|
901
|
+
known_phase - measured_phase,
|
902
|
+
<float_t> (NAN if known_modulation == 0.0 else INFINITY)
|
903
|
+
)
|
904
|
+
return known_phase - measured_phase, known_modulation / measured_modulation
|
905
|
+
|
906
|
+
|
907
|
+
@cython.ufunc
|
908
|
+
cdef (float_t, float_t) _polar_from_reference_phasor(
|
909
|
+
float_t measured_real,
|
910
|
+
float_t measured_imag,
|
911
|
+
float_t known_real,
|
912
|
+
float_t known_imag,
|
913
|
+
) noexcept nogil:
|
914
|
+
"""Return polar coordinates for calibration from reference phasor."""
|
915
|
+
cdef:
|
916
|
+
double measured_phase, measured_modulation
|
917
|
+
double known_phase, known_modulation
|
918
|
+
|
919
|
+
if (
|
920
|
+
isnan(measured_real)
|
921
|
+
or isnan(measured_imag)
|
922
|
+
or isnan(known_real)
|
923
|
+
or isnan(known_imag)
|
924
|
+
):
|
925
|
+
return <float_t> NAN, <float_t> NAN
|
926
|
+
|
927
|
+
measured_phase = atan2(measured_imag, measured_real)
|
928
|
+
known_phase = atan2(known_imag, known_real)
|
929
|
+
measured_modulation = hypot(measured_real, measured_imag)
|
930
|
+
known_modulation = hypot(known_real, known_imag)
|
931
|
+
|
932
|
+
if fabs(measured_modulation) == 0.0:
|
933
|
+
# return <float_t> (known_phase - measured_phase), <float_t> INFINITY
|
934
|
+
return (
|
935
|
+
<float_t> (known_phase - measured_phase),
|
936
|
+
<float_t> (NAN if known_modulation == 0.0 else INFINITY)
|
937
|
+
)
|
938
|
+
return (
|
939
|
+
<float_t> (known_phase - measured_phase),
|
940
|
+
<float_t> (known_modulation / measured_modulation)
|
941
|
+
)
|
942
|
+
|
943
|
+
|
944
|
+
@cython.ufunc
|
945
|
+
cdef (float_t, float_t) _phasor_at_harmonic(
|
946
|
+
float_t real,
|
947
|
+
int harmonic,
|
948
|
+
int other_harmonic,
|
949
|
+
) noexcept nogil:
|
950
|
+
"""Return phasor coordinates on universal semicircle at other harmonic."""
|
951
|
+
if isnan(real):
|
952
|
+
return <float_t> NAN, <float_t> NAN
|
953
|
+
|
954
|
+
if real <= 0.0:
|
955
|
+
return 0.0, 0.0
|
956
|
+
if real >= 1.0:
|
957
|
+
return 1.0, 0.0
|
958
|
+
|
959
|
+
harmonic *= harmonic
|
960
|
+
other_harmonic *= other_harmonic
|
961
|
+
real = (
|
962
|
+
harmonic * real / (other_harmonic + (harmonic - other_harmonic) * real)
|
963
|
+
)
|
964
|
+
|
965
|
+
return real, <float_t> sqrt(real - real * real)
|
966
|
+
|
967
|
+
|
968
|
+
@cython.ufunc
|
969
|
+
cdef (float_t, float_t) _phasor_multiply(
|
970
|
+
float_t real,
|
971
|
+
float_t imag,
|
972
|
+
float_t real2,
|
973
|
+
float_t imag2,
|
974
|
+
) noexcept nogil:
|
975
|
+
"""Return complex multiplication of two phasors."""
|
976
|
+
return (
|
977
|
+
real * real2 - imag * imag2,
|
978
|
+
real * imag2 + imag * real2
|
979
|
+
)
|
980
|
+
|
981
|
+
|
982
|
+
@cython.ufunc
|
983
|
+
cdef (float_t, float_t) _phasor_divide(
|
984
|
+
float_t real,
|
985
|
+
float_t imag,
|
986
|
+
float_t real2,
|
987
|
+
float_t imag2,
|
988
|
+
) noexcept nogil:
|
989
|
+
"""Return complex division of two phasors."""
|
990
|
+
cdef:
|
991
|
+
float_t divisor = real2 * real2 + imag2 * imag2
|
992
|
+
|
993
|
+
if divisor != 0.0:
|
994
|
+
# includes isnan(divisor)
|
995
|
+
return (
|
996
|
+
(real * real2 + imag * imag2) / divisor,
|
997
|
+
(imag * real2 - real * imag2) / divisor
|
998
|
+
)
|
999
|
+
|
1000
|
+
real = real * real2 + imag * imag2
|
1001
|
+
imag = imag * real2 - real * imag2
|
1002
|
+
return (
|
1003
|
+
NAN if real == 0.0 else real * INFINITY,
|
1004
|
+
NAN if imag == 0.0 else imag * INFINITY
|
1005
|
+
)
|
1006
|
+
|
1007
|
+
|
1008
|
+
###############################################################################
|
1009
|
+
# Geometry ufuncs
|
1010
|
+
|
1011
|
+
|
1012
|
+
@cython.ufunc
|
1013
|
+
cdef unsigned char _is_inside_range(
|
1014
|
+
float_t x, # point
|
1015
|
+
float_t y,
|
1016
|
+
float_t xmin, # x range
|
1017
|
+
float_t xmax,
|
1018
|
+
float_t ymin, # y range
|
1019
|
+
float_t ymax
|
1020
|
+
) noexcept nogil:
|
1021
|
+
"""Return whether point is inside range.
|
1022
|
+
|
1023
|
+
Range includes lower but not upper limit.
|
1024
|
+
|
1025
|
+
"""
|
1026
|
+
if isnan(x) or isnan(y):
|
1027
|
+
return False
|
1028
|
+
|
1029
|
+
return x >= xmin and x < xmax and y >= ymin and y < ymax
|
1030
|
+
|
1031
|
+
|
1032
|
+
@cython.ufunc
|
1033
|
+
cdef unsigned char _is_inside_rectangle(
|
1034
|
+
float_t x, # point
|
1035
|
+
float_t y,
|
1036
|
+
float_t x0, # segment start
|
1037
|
+
float_t y0,
|
1038
|
+
float_t x1, # segment end
|
1039
|
+
float_t y1,
|
1040
|
+
float_t r, # half width
|
1041
|
+
) noexcept nogil:
|
1042
|
+
"""Return whether point is in rectangle.
|
1043
|
+
|
1044
|
+
The rectangle is defined by central line segment and half width.
|
1045
|
+
|
1046
|
+
"""
|
1047
|
+
cdef:
|
1048
|
+
float_t t
|
1049
|
+
|
1050
|
+
if r <= 0.0 or isnan(x) or isnan(y):
|
1051
|
+
return False
|
1052
|
+
|
1053
|
+
# normalize coordinates
|
1054
|
+
# x1 = 0
|
1055
|
+
# y1 = 0
|
1056
|
+
x0 -= x1
|
1057
|
+
y0 -= y1
|
1058
|
+
x -= x1
|
1059
|
+
y -= y1
|
1060
|
+
# square of line length
|
1061
|
+
t = x0 * x0 + y0 * y0
|
1062
|
+
if t <= 0.0:
|
1063
|
+
return x * x + y * y <= r * r
|
1064
|
+
# projection of point on line using clamped dot product
|
1065
|
+
t = (x * x0 + y * y0) / t
|
1066
|
+
if t < 0.0 or t > 1.0:
|
1067
|
+
return False
|
1068
|
+
# compare square of lengths of projection and radius
|
1069
|
+
x -= t * x0
|
1070
|
+
y -= t * y0
|
1071
|
+
return x * x + y * y <= r * r
|
1072
|
+
|
1073
|
+
|
1074
|
+
@cython.ufunc
|
1075
|
+
cdef unsigned char _is_inside_polar_rectangle(
|
1076
|
+
float_t x, # point
|
1077
|
+
float_t y,
|
1078
|
+
float_t angle_min, # phase, -pi to pi
|
1079
|
+
float_t angle_max,
|
1080
|
+
float_t distance_min, # modulation
|
1081
|
+
float_t distance_max,
|
1082
|
+
) noexcept nogil:
|
1083
|
+
"""Return whether point is inside polar rectangle.
|
1084
|
+
|
1085
|
+
Angles should be in range [-pi, pi], else performance is degraded.
|
1086
|
+
|
1087
|
+
"""
|
1088
|
+
cdef:
|
1089
|
+
double t
|
1090
|
+
|
1091
|
+
if isnan(x) or isnan(y):
|
1092
|
+
return False
|
1093
|
+
|
1094
|
+
if distance_min > distance_max:
|
1095
|
+
distance_min, distance_max = distance_max, distance_min
|
1096
|
+
t = hypot(x, y)
|
1097
|
+
if t < distance_min or t > distance_max or t == 0.0:
|
1098
|
+
return False
|
1099
|
+
|
1100
|
+
if angle_min < -M_PI or angle_min > M_PI:
|
1101
|
+
angle_min = <float_t> atan2(sin(angle_min), cos(angle_min))
|
1102
|
+
if angle_max < -M_PI or angle_max > M_PI:
|
1103
|
+
angle_max = <float_t> atan2(sin(angle_max), cos(angle_max))
|
1104
|
+
if angle_min > angle_max:
|
1105
|
+
angle_min, angle_max = angle_max, angle_min
|
1106
|
+
t = <float_t> atan2(y, x)
|
1107
|
+
if t < angle_min or t > angle_max:
|
1108
|
+
return False
|
1109
|
+
|
1110
|
+
return True
|
1111
|
+
|
1112
|
+
|
1113
|
+
@cython.ufunc
|
1114
|
+
cdef unsigned char _is_inside_circle(
|
1115
|
+
float_t x, # point
|
1116
|
+
float_t y,
|
1117
|
+
float_t x0, # circle center
|
1118
|
+
float_t y0,
|
1119
|
+
float_t r, # circle radius
|
1120
|
+
) noexcept nogil:
|
1121
|
+
"""Return whether point is inside circle."""
|
1122
|
+
if r <= 0.0 or isnan(x) or isnan(y):
|
1123
|
+
return False
|
1124
|
+
|
1125
|
+
x -= x0
|
1126
|
+
y -= y0
|
1127
|
+
return x * x + y * y <= r * r
|
1128
|
+
|
1129
|
+
|
1130
|
+
@cython.ufunc
|
1131
|
+
cdef unsigned char _is_inside_ellipse(
|
1132
|
+
float_t x, # point
|
1133
|
+
float_t y,
|
1134
|
+
float_t x0, # ellipse center
|
1135
|
+
float_t y0,
|
1136
|
+
float_t a, # ellipse radii
|
1137
|
+
float_t b,
|
1138
|
+
float_t phi, # ellipse angle
|
1139
|
+
) noexcept nogil:
|
1140
|
+
"""Return whether point is inside ellipse.
|
1141
|
+
|
1142
|
+
Same as _is_inside_circle if a == b.
|
1143
|
+
Consider using _is_inside_ellipse_ instead, which should be faster
|
1144
|
+
for arrays.
|
1145
|
+
|
1146
|
+
"""
|
1147
|
+
cdef:
|
1148
|
+
float_t sina, cosa
|
1149
|
+
|
1150
|
+
if a <= 0.0 or b <= 0.0 or isnan(x) or isnan(y):
|
1151
|
+
return False
|
1152
|
+
|
1153
|
+
x -= x0
|
1154
|
+
y -= y0
|
1155
|
+
if a == b:
|
1156
|
+
# circle
|
1157
|
+
return x * x + y * y <= a * a
|
1158
|
+
sina = <float_t> sin(phi)
|
1159
|
+
cosa = <float_t> cos(phi)
|
1160
|
+
x0 = (cosa * x + sina * y) / a
|
1161
|
+
y0 = (sina * x - cosa * y) / b
|
1162
|
+
return x0 * x0 + y0 * y0 <= 1.0
|
1163
|
+
|
1164
|
+
|
1165
|
+
@cython.ufunc
|
1166
|
+
cdef unsigned char _is_inside_ellipse_(
|
1167
|
+
float_t x, # point
|
1168
|
+
float_t y,
|
1169
|
+
float_t x0, # ellipse center
|
1170
|
+
float_t y0,
|
1171
|
+
float_t a, # ellipse radii
|
1172
|
+
float_t b,
|
1173
|
+
float_t sina, # sin/cos of ellipse angle
|
1174
|
+
float_t cosa,
|
1175
|
+
) noexcept nogil:
|
1176
|
+
"""Return whether point is inside ellipse.
|
1177
|
+
|
1178
|
+
Use pre-calculated sin(angle) and cos(angle).
|
1179
|
+
|
1180
|
+
"""
|
1181
|
+
if a <= 0.0 or b <= 0.0 or isnan(x) or isnan(y):
|
1182
|
+
return False
|
1183
|
+
|
1184
|
+
x -= x0
|
1185
|
+
y -= y0
|
1186
|
+
if a == b:
|
1187
|
+
# circle
|
1188
|
+
return x * x + y * y <= a * a
|
1189
|
+
x0 = (cosa * x + sina * y) / a
|
1190
|
+
y0 = (sina * x - cosa * y) / b
|
1191
|
+
return x0 * x0 + y0 * y0 <= 1.0
|
1192
|
+
|
1193
|
+
|
1194
|
+
@cython.ufunc
|
1195
|
+
cdef unsigned char _is_inside_stadium(
|
1196
|
+
float_t x, # point
|
1197
|
+
float_t y,
|
1198
|
+
float_t x0, # line start
|
1199
|
+
float_t y0,
|
1200
|
+
float_t x1, # line end
|
1201
|
+
float_t y1,
|
1202
|
+
float_t r, # radius
|
1203
|
+
) noexcept nogil:
|
1204
|
+
"""Return whether point is inside stadium.
|
1205
|
+
|
1206
|
+
A stadium shape is a thick line with rounded ends.
|
1207
|
+
Same as _is_near_segment.
|
1208
|
+
|
1209
|
+
"""
|
1210
|
+
cdef:
|
1211
|
+
float_t t
|
1212
|
+
|
1213
|
+
if r <= 0.0 or isnan(x) or isnan(y):
|
1214
|
+
return False
|
1215
|
+
|
1216
|
+
# normalize coordinates
|
1217
|
+
# x1 = 0
|
1218
|
+
# y1 = 0
|
1219
|
+
x0 -= x1
|
1220
|
+
y0 -= y1
|
1221
|
+
x -= x1
|
1222
|
+
y -= y1
|
1223
|
+
# square of line length
|
1224
|
+
t = x0 * x0 + y0 * y0
|
1225
|
+
if t <= 0.0:
|
1226
|
+
return x * x + y * y <= r * r
|
1227
|
+
# projection of point on line using clamped dot product
|
1228
|
+
t = (x * x0 + y * y0) / t
|
1229
|
+
t = <float_t> max(0.0, min(1.0, t))
|
1230
|
+
# compare square of lengths of projection and radius
|
1231
|
+
x -= t * x0
|
1232
|
+
y -= t * y0
|
1233
|
+
return x * x + y * y <= r * r
|
1234
|
+
|
1235
|
+
|
1236
|
+
# function alias
|
1237
|
+
_is_near_segment = _is_inside_stadium
|
1238
|
+
|
1239
|
+
|
1240
|
+
@cython.ufunc
|
1241
|
+
cdef unsigned char _is_inside_semicircle(
|
1242
|
+
float_t x, # point
|
1243
|
+
float_t y,
|
1244
|
+
float_t r, # distance
|
1245
|
+
) noexcept nogil:
|
1246
|
+
"""Return whether point is inside universal semicircle."""
|
1247
|
+
if r < 0.0 or isnan(x) or isnan(y):
|
1248
|
+
return False
|
1249
|
+
if y < -r:
|
1250
|
+
return False
|
1251
|
+
if y <= 0.0:
|
1252
|
+
if x >= 0.0 and x <= 1.0:
|
1253
|
+
return True
|
1254
|
+
# near endpoints?
|
1255
|
+
if x > 0.5:
|
1256
|
+
x -= <float_t> 1.0
|
1257
|
+
return x * x + y * y <= r * r
|
1258
|
+
return hypot(x - 0.5, y) <= r + 0.5
|
1259
|
+
|
1260
|
+
|
1261
|
+
@cython.ufunc
|
1262
|
+
cdef unsigned char _is_near_semicircle(
|
1263
|
+
float_t x, # point
|
1264
|
+
float_t y,
|
1265
|
+
float_t r, # distance
|
1266
|
+
) noexcept nogil:
|
1267
|
+
"""Return whether point is near universal semicircle."""
|
1268
|
+
if r < 0.0 or isnan(x) or isnan(y):
|
1269
|
+
return False
|
1270
|
+
if y < 0.0:
|
1271
|
+
# near endpoints?
|
1272
|
+
if x > 0.5:
|
1273
|
+
x -= <float_t> 1.0
|
1274
|
+
return x * x + y * y <= r * r
|
1275
|
+
return fabs(hypot(x - 0.5, y) - 0.5) <= r
|
1276
|
+
|
1277
|
+
|
1278
|
+
@cython.ufunc
|
1279
|
+
cdef unsigned char _is_near_line(
|
1280
|
+
float_t x, # point
|
1281
|
+
float_t y,
|
1282
|
+
float_t x0, # line start
|
1283
|
+
float_t y0,
|
1284
|
+
float_t x1, # line end
|
1285
|
+
float_t y1,
|
1286
|
+
float_t r, # distance
|
1287
|
+
) noexcept nogil:
|
1288
|
+
"""Return whether point is close to line."""
|
1289
|
+
cdef:
|
1290
|
+
float_t t
|
1291
|
+
|
1292
|
+
if r <= 0.0 or isnan(x) or isnan(y):
|
1293
|
+
return False
|
1294
|
+
|
1295
|
+
# normalize coordinates
|
1296
|
+
# x1 = 0
|
1297
|
+
# y1 = 0
|
1298
|
+
x0 -= x1
|
1299
|
+
y0 -= y1
|
1300
|
+
x -= x1
|
1301
|
+
y -= y1
|
1302
|
+
# square of line length
|
1303
|
+
t = x0 * x0 + y0 * y0
|
1304
|
+
if t <= 0.0:
|
1305
|
+
return x * x + y * y <= r * r
|
1306
|
+
# projection of point on line using clamped dot product
|
1307
|
+
t = (x * x0 + y * y0) / t
|
1308
|
+
# compare square of lengths of projection and radius
|
1309
|
+
x -= t * x0
|
1310
|
+
y -= t * y0
|
1311
|
+
return x * x + y * y <= r * r
|
1312
|
+
|
1313
|
+
|
1314
|
+
@cython.ufunc
|
1315
|
+
cdef (float_t, float_t) _point_on_segment(
|
1316
|
+
float_t x, # point
|
1317
|
+
float_t y,
|
1318
|
+
float_t x0, # segment start
|
1319
|
+
float_t y0,
|
1320
|
+
float_t x1, # segment end
|
1321
|
+
float_t y1,
|
1322
|
+
) noexcept nogil:
|
1323
|
+
"""Return point projected onto line segment."""
|
1324
|
+
cdef:
|
1325
|
+
float_t t
|
1326
|
+
|
1327
|
+
if isnan(x) or isnan(y):
|
1328
|
+
return <float_t> NAN, <float_t> NAN
|
1329
|
+
|
1330
|
+
# normalize coordinates
|
1331
|
+
# x1 = 0
|
1332
|
+
# y1 = 0
|
1333
|
+
x0 -= x1
|
1334
|
+
y0 -= y1
|
1335
|
+
x -= x1
|
1336
|
+
y -= y1
|
1337
|
+
# square of line length
|
1338
|
+
t = x0 * x0 + y0 * y0
|
1339
|
+
if t <= 0.0:
|
1340
|
+
return x0, y0
|
1341
|
+
# projection of point on line
|
1342
|
+
t = (x * x0 + y * y0) / t
|
1343
|
+
# clamp to line segment
|
1344
|
+
if t < 0.0:
|
1345
|
+
t = 0.0
|
1346
|
+
elif t > 1.0:
|
1347
|
+
t = 1.0
|
1348
|
+
x1 += t * x0
|
1349
|
+
y1 += t * y0
|
1350
|
+
return x1, y1
|
1351
|
+
|
1352
|
+
|
1353
|
+
@cython.ufunc
|
1354
|
+
cdef (float_t, float_t) _point_on_line(
|
1355
|
+
float_t x, # point
|
1356
|
+
float_t y,
|
1357
|
+
float_t x0, # line start
|
1358
|
+
float_t y0,
|
1359
|
+
float_t x1, # line end
|
1360
|
+
float_t y1,
|
1361
|
+
) noexcept nogil:
|
1362
|
+
"""Return point projected onto line."""
|
1363
|
+
cdef:
|
1364
|
+
float_t t
|
1365
|
+
|
1366
|
+
if isnan(x) or isnan(y):
|
1367
|
+
return <float_t> NAN, <float_t> NAN
|
1368
|
+
|
1369
|
+
# normalize coordinates
|
1370
|
+
# x1 = 0
|
1371
|
+
# y1 = 0
|
1372
|
+
x0 -= x1
|
1373
|
+
y0 -= y1
|
1374
|
+
x -= x1
|
1375
|
+
y -= y1
|
1376
|
+
# square of line length
|
1377
|
+
t = x0 * x0 + y0 * y0
|
1378
|
+
if t <= 0.0:
|
1379
|
+
return x0, y0
|
1380
|
+
# projection of point on line
|
1381
|
+
t = (x * x0 + y * y0) / t
|
1382
|
+
x1 += t * x0
|
1383
|
+
y1 += t * y0
|
1384
|
+
return x1, y1
|
1385
|
+
|
1386
|
+
|
1387
|
+
@cython.ufunc
|
1388
|
+
cdef float_t _fraction_on_segment(
|
1389
|
+
float_t x, # point
|
1390
|
+
float_t y,
|
1391
|
+
float_t x0, # segment start
|
1392
|
+
float_t y0,
|
1393
|
+
float_t x1, # segment end
|
1394
|
+
float_t y1,
|
1395
|
+
) noexcept nogil:
|
1396
|
+
"""Return normalized fraction of point projected onto line segment."""
|
1397
|
+
cdef:
|
1398
|
+
float_t t
|
1399
|
+
|
1400
|
+
if isnan(x) or isnan(y):
|
1401
|
+
return <float_t> NAN
|
1402
|
+
|
1403
|
+
# normalize coordinates
|
1404
|
+
x -= x1
|
1405
|
+
y -= y1
|
1406
|
+
x0 -= x1
|
1407
|
+
y0 -= y1
|
1408
|
+
# x1 = 0
|
1409
|
+
# y1 = 0
|
1410
|
+
# square of line length
|
1411
|
+
t = x0 * x0 + y0 * y0
|
1412
|
+
if t <= 0.0:
|
1413
|
+
# not a line segment
|
1414
|
+
return 0.0
|
1415
|
+
# projection of point on line
|
1416
|
+
t = (x * x0 + y * y0) / t
|
1417
|
+
# clamp to line segment
|
1418
|
+
if t < 0.0:
|
1419
|
+
t = 0.0
|
1420
|
+
elif t > 1.0:
|
1421
|
+
t = 1.0
|
1422
|
+
return t
|
1423
|
+
|
1424
|
+
|
1425
|
+
@cython.ufunc
|
1426
|
+
cdef float_t _fraction_on_line(
|
1427
|
+
float_t x, # point
|
1428
|
+
float_t y,
|
1429
|
+
float_t x0, # line start
|
1430
|
+
float_t y0,
|
1431
|
+
float_t x1, # line end
|
1432
|
+
float_t y1,
|
1433
|
+
) noexcept nogil:
|
1434
|
+
"""Return normalized fraction of point projected onto line."""
|
1435
|
+
cdef:
|
1436
|
+
float_t t
|
1437
|
+
|
1438
|
+
if isnan(x) or isnan(y):
|
1439
|
+
return <float_t> NAN
|
1440
|
+
|
1441
|
+
# normalize coordinates
|
1442
|
+
x -= x1
|
1443
|
+
y -= y1
|
1444
|
+
x0 -= x1
|
1445
|
+
y0 -= y1
|
1446
|
+
# x1 = 0
|
1447
|
+
# y1 = 0
|
1448
|
+
# square of line length
|
1449
|
+
t = x0 * x0 + y0 * y0
|
1450
|
+
if t <= 0.0:
|
1451
|
+
# not a line segment
|
1452
|
+
return 1.0
|
1453
|
+
# projection of point on line
|
1454
|
+
t = (x * x0 + y * y0) / t
|
1455
|
+
return t
|
1456
|
+
|
1457
|
+
|
1458
|
+
@cython.ufunc
|
1459
|
+
cdef float_t _distance_from_point(
|
1460
|
+
float_t x, # point
|
1461
|
+
float_t y,
|
1462
|
+
float_t x0, # other point
|
1463
|
+
float_t y0,
|
1464
|
+
) noexcept nogil:
|
1465
|
+
"""Return distance from point."""
|
1466
|
+
if isnan(x) or isnan(y): # or isnan(x0) or isnan(y0)
|
1467
|
+
return <float_t> NAN
|
1468
|
+
|
1469
|
+
return <float_t> hypot(x - x0, y - y0)
|
1470
|
+
|
1471
|
+
|
1472
|
+
@cython.ufunc
|
1473
|
+
cdef float_t _distance_from_segment(
|
1474
|
+
float_t x, # point
|
1475
|
+
float_t y,
|
1476
|
+
float_t x0, # segment start
|
1477
|
+
float_t y0,
|
1478
|
+
float_t x1, # segment end
|
1479
|
+
float_t y1,
|
1480
|
+
) noexcept nogil:
|
1481
|
+
"""Return distance from segment."""
|
1482
|
+
cdef:
|
1483
|
+
float_t t
|
1484
|
+
|
1485
|
+
if isnan(x) or isnan(y):
|
1486
|
+
return <float_t> NAN
|
1487
|
+
|
1488
|
+
# normalize coordinates
|
1489
|
+
# x1 = 0
|
1490
|
+
# y1 = 0
|
1491
|
+
x0 -= x1
|
1492
|
+
y0 -= y1
|
1493
|
+
x -= x1
|
1494
|
+
y -= y1
|
1495
|
+
# square of line length
|
1496
|
+
t = x0 * x0 + y0 * y0
|
1497
|
+
if t <= 0.0:
|
1498
|
+
return <float_t> hypot(x, y)
|
1499
|
+
# projection of point on line using dot product
|
1500
|
+
t = (x * x0 + y * y0) / t
|
1501
|
+
if t > 1.0:
|
1502
|
+
x -= x0
|
1503
|
+
y -= y0
|
1504
|
+
elif t > 0.0:
|
1505
|
+
x -= t * x0
|
1506
|
+
y -= t * y0
|
1507
|
+
return <float_t> hypot(x, y)
|
1508
|
+
|
1509
|
+
|
1510
|
+
@cython.ufunc
|
1511
|
+
cdef float_t _distance_from_line(
|
1512
|
+
float_t x, # point
|
1513
|
+
float_t y,
|
1514
|
+
float_t x0, # line start
|
1515
|
+
float_t y0,
|
1516
|
+
float_t x1, # line end
|
1517
|
+
float_t y1,
|
1518
|
+
) noexcept nogil:
|
1519
|
+
"""Return distance from line."""
|
1520
|
+
cdef:
|
1521
|
+
float_t t
|
1522
|
+
|
1523
|
+
if isnan(x) or isnan(y):
|
1524
|
+
return <float_t> NAN
|
1525
|
+
|
1526
|
+
# normalize coordinates
|
1527
|
+
# x1 = 0
|
1528
|
+
# y1 = 0
|
1529
|
+
x0 -= x1
|
1530
|
+
y0 -= y1
|
1531
|
+
x -= x1
|
1532
|
+
y -= y1
|
1533
|
+
# square of line length
|
1534
|
+
t = x0 * x0 + y0 * y0
|
1535
|
+
if t <= 0.0:
|
1536
|
+
return <float_t> hypot(x, y)
|
1537
|
+
# projection of point on line using dot product
|
1538
|
+
t = (x * x0 + y * y0) / t
|
1539
|
+
x -= t * x0
|
1540
|
+
y -= t * y0
|
1541
|
+
return <float_t> hypot(x, y)
|
1542
|
+
|
1543
|
+
|
1544
|
+
@cython.ufunc
|
1545
|
+
cdef float_t _distance_from_semicircle(
|
1546
|
+
float_t x, # point
|
1547
|
+
float_t y,
|
1548
|
+
) noexcept nogil:
|
1549
|
+
"""Return distance from universal semicircle."""
|
1550
|
+
if isnan(x) or isnan(y):
|
1551
|
+
return NAN
|
1552
|
+
if y < 0.0:
|
1553
|
+
# distance to endpoints
|
1554
|
+
if x > 0.5:
|
1555
|
+
x -= <float_t> 1.0
|
1556
|
+
return <float_t> hypot(x, y)
|
1557
|
+
return <float_t> fabs(hypot(x - 0.5, y) - 0.5)
|
1558
|
+
|
1559
|
+
|
1560
|
+
@cython.ufunc
|
1561
|
+
cdef (float_t, float_t, float_t) _segment_direction_and_length(
|
1562
|
+
float_t x0, # segment start
|
1563
|
+
float_t y0,
|
1564
|
+
float_t x1, # segment end
|
1565
|
+
float_t y1,
|
1566
|
+
) noexcept nogil:
|
1567
|
+
"""Return direction and length of line segment."""
|
1568
|
+
cdef:
|
1569
|
+
float_t length
|
1570
|
+
|
1571
|
+
if isnan(x0) or isnan(y0) or isnan(x1) or isnan(y1):
|
1572
|
+
return NAN, NAN, 0.0
|
1573
|
+
|
1574
|
+
x1 -= x0
|
1575
|
+
y1 -= y0
|
1576
|
+
length = <float_t> hypot(x1, y1)
|
1577
|
+
if length <= 0.0:
|
1578
|
+
return NAN, NAN, 0.0
|
1579
|
+
x1 /= length
|
1580
|
+
y1 /= length
|
1581
|
+
return x1, y1, length
|
1582
|
+
|
1583
|
+
|
1584
|
+
@cython.ufunc
|
1585
|
+
cdef (float_t, float_t, float_t, float_t) _intersect_circle_circle(
|
1586
|
+
float_t x0, # circle 0
|
1587
|
+
float_t y0,
|
1588
|
+
float_t r0,
|
1589
|
+
float_t x1, # circle 1
|
1590
|
+
float_t y1,
|
1591
|
+
float_t r1,
|
1592
|
+
) noexcept nogil:
|
1593
|
+
"""Return coordinates of intersections of two circles."""
|
1594
|
+
cdef:
|
1595
|
+
double dx, dy, dr, ll, dd, hd, ld
|
1596
|
+
|
1597
|
+
if (
|
1598
|
+
isnan(x0)
|
1599
|
+
or isnan(y0)
|
1600
|
+
or isnan(r0)
|
1601
|
+
or isnan(x1)
|
1602
|
+
or isnan(y1)
|
1603
|
+
or isnan(r1)
|
1604
|
+
or r0 == 0.0
|
1605
|
+
or r1 == 0.0
|
1606
|
+
):
|
1607
|
+
return NAN, NAN, NAN, NAN
|
1608
|
+
|
1609
|
+
dx = x1 - x0
|
1610
|
+
dy = y1 - y0
|
1611
|
+
dr = hypot(dx, dy)
|
1612
|
+
if dr <= 0.0:
|
1613
|
+
# circle positions identical
|
1614
|
+
return NAN, NAN, NAN, NAN
|
1615
|
+
ll = (r0 * r0 - r1 * r1 + dr * dr) / (dr + dr)
|
1616
|
+
dd = r0 * r0 - ll * ll
|
1617
|
+
if dd < 0.0 or dr <= 0.0:
|
1618
|
+
# circles not intersecting
|
1619
|
+
return NAN, NAN, NAN, NAN
|
1620
|
+
hd = sqrt(dd) / dr
|
1621
|
+
ld = ll / dr
|
1622
|
+
return (
|
1623
|
+
<float_t> (ld * dx + hd * dy + x0),
|
1624
|
+
<float_t> (ld * dy - hd * dx + y0),
|
1625
|
+
<float_t> (ld * dx - hd * dy + x0),
|
1626
|
+
<float_t> (ld * dy + hd * dx + y0),
|
1627
|
+
)
|
1628
|
+
|
1629
|
+
|
1630
|
+
@cython.ufunc
|
1631
|
+
cdef (float_t, float_t, float_t, float_t) _intersect_circle_line(
|
1632
|
+
float_t x, # circle
|
1633
|
+
float_t y,
|
1634
|
+
float_t r,
|
1635
|
+
float_t x0, # line start
|
1636
|
+
float_t y0,
|
1637
|
+
float_t x1, # line end
|
1638
|
+
float_t y1,
|
1639
|
+
) noexcept nogil:
|
1640
|
+
"""Return coordinates of intersections of circle and line."""
|
1641
|
+
cdef:
|
1642
|
+
double dx, dy, dr, dd, rdd
|
1643
|
+
|
1644
|
+
if (
|
1645
|
+
isnan(r)
|
1646
|
+
or isnan(x)
|
1647
|
+
or isnan(y)
|
1648
|
+
or isnan(x0)
|
1649
|
+
or isnan(y0)
|
1650
|
+
or isnan(x1)
|
1651
|
+
or isnan(y1)
|
1652
|
+
or r == 0.0
|
1653
|
+
):
|
1654
|
+
return NAN, NAN, NAN, NAN
|
1655
|
+
|
1656
|
+
dx = x1 - x0
|
1657
|
+
dy = y1 - y0
|
1658
|
+
dr = dx * dx + dy * dy
|
1659
|
+
dd = (x0 - x) * (y1 - y) - (x1 - x) * (y0 - y)
|
1660
|
+
rdd = r * r * dr - dd * dd # discriminant
|
1661
|
+
if rdd < 0.0 or dr <= 0.0:
|
1662
|
+
# no intersection
|
1663
|
+
return NAN, NAN, NAN, NAN
|
1664
|
+
rdd = sqrt(rdd)
|
1665
|
+
return (
|
1666
|
+
x + <float_t> ((dd * dy + copysign(1.0, dy) * dx * rdd) / dr),
|
1667
|
+
y + <float_t> ((-dd * dx + fabs(dy) * rdd) / dr),
|
1668
|
+
x + <float_t> ((dd * dy - copysign(1.0, dy) * dx * rdd) / dr),
|
1669
|
+
y + <float_t> ((-dd * dx - fabs(dy) * rdd) / dr),
|
1670
|
+
)
|
1671
|
+
|
1672
|
+
|
1673
|
+
@cython.ufunc
|
1674
|
+
cdef (float_t, float_t, float_t, float_t) _intersect_semicircle_line(
|
1675
|
+
float_t x0, # line start
|
1676
|
+
float_t y0,
|
1677
|
+
float_t x1, # line end
|
1678
|
+
float_t y1,
|
1679
|
+
) noexcept nogil:
|
1680
|
+
"""Return coordinates of intersections of line and universal semicircle."""
|
1681
|
+
cdef:
|
1682
|
+
double dx, dy, dr, dd, rdd
|
1683
|
+
|
1684
|
+
if isnan(x0) or isnan(x1) or isnan(y0) or isnan(y1):
|
1685
|
+
return NAN, NAN, NAN, NAN
|
1686
|
+
|
1687
|
+
dx = x1 - x0
|
1688
|
+
dy = y1 - y0
|
1689
|
+
dr = dx * dx + dy * dy
|
1690
|
+
dd = (x0 - 0.5) * y1 - (x1 - 0.5) * y0
|
1691
|
+
rdd = 0.25 * dr - dd * dd # discriminant
|
1692
|
+
if rdd < 0.0 or dr <= 0.0:
|
1693
|
+
# no intersection
|
1694
|
+
return NAN, NAN, NAN, NAN
|
1695
|
+
rdd = sqrt(rdd)
|
1696
|
+
x0 = <float_t> ((dd * dy - copysign(1.0, dy) * dx * rdd) / dr + 0.5)
|
1697
|
+
y0 = <float_t> ((-dd * dx - fabs(dy) * rdd) / dr)
|
1698
|
+
x1 = <float_t> ((dd * dy + copysign(1.0, dy) * dx * rdd) / dr + 0.5)
|
1699
|
+
y1 = <float_t> ((-dd * dx + fabs(dy) * rdd) / dr)
|
1700
|
+
if y0 < 0.0:
|
1701
|
+
x0 = NAN
|
1702
|
+
y0 = NAN
|
1703
|
+
if y1 < 0.0:
|
1704
|
+
x1 = NAN
|
1705
|
+
y1 = NAN
|
1706
|
+
return x0, y0, x1, y1
|
1707
|
+
|
1708
|
+
|
1709
|
+
###############################################################################
|
1710
|
+
# Search functions
|
1711
|
+
|
1712
|
+
|
1713
|
+
def _lifetime_search_2(
|
1714
|
+
float_t[:, ::] lifetime, # (num_components, pixels)
|
1715
|
+
float_t[:, ::] fraction, # (num_components, pixels)
|
1716
|
+
const float_t[:, ::] real, # (num_components, pixels)
|
1717
|
+
const float_t[:, ::] imag, # (num_components, pixels)
|
1718
|
+
const double[::] candidate, # real coordinates to scan
|
1719
|
+
const double omega_sqr,
|
1720
|
+
const int num_threads
|
1721
|
+
):
|
1722
|
+
"""Find two lifetime components and fractions in harmonic coordinates.
|
1723
|
+
|
1724
|
+
https://doi.org/10.1021/acs.jpcb.0c06946
|
1725
|
+
|
1726
|
+
"""
|
1727
|
+
cdef:
|
1728
|
+
ssize_t i, u
|
1729
|
+
double re1, im1, re2, im2
|
1730
|
+
double g0, g1, g0h1, s0h1, g1h1, s1h1, g0h2, s0h2, g1h2, s1h2
|
1731
|
+
double x, y, dx, dy, dr, dd, rdd
|
1732
|
+
double dmin, d, f, t
|
1733
|
+
|
1734
|
+
if lifetime.shape[0] != 2 or lifetime.shape[1] != real.shape[1]:
|
1735
|
+
raise ValueError('lifetime shape invalid')
|
1736
|
+
if fraction.shape[0] != 2 or fraction.shape[1] != real.shape[1]:
|
1737
|
+
raise ValueError('fraction shape invalid')
|
1738
|
+
if real.shape[0] != imag.shape[0] != 2:
|
1739
|
+
raise ValueError('phasor harmonics invalid')
|
1740
|
+
if real.shape[1] != imag.shape[1]:
|
1741
|
+
raise ValueError('phasor size invalid')
|
1742
|
+
if candidate.shape[0] < 1:
|
1743
|
+
raise ValueError('candidate size < 1')
|
1744
|
+
|
1745
|
+
with nogil, parallel(num_threads=num_threads):
|
1746
|
+
|
1747
|
+
for u in prange(real.shape[1]):
|
1748
|
+
# loop over phasor coordinates
|
1749
|
+
re1 = real[0, u]
|
1750
|
+
re2 = real[1, u]
|
1751
|
+
im1 = imag[0, u]
|
1752
|
+
im2 = imag[1, u]
|
1753
|
+
|
1754
|
+
if (
|
1755
|
+
isnan(re1)
|
1756
|
+
or isnan(im1)
|
1757
|
+
or isnan(re2)
|
1758
|
+
or isnan(im2)
|
1759
|
+
# outside semicircle?
|
1760
|
+
or re1 < 0.0
|
1761
|
+
or re2 < 0.0
|
1762
|
+
or re1 > 1.0
|
1763
|
+
or re2 > 1.0
|
1764
|
+
or im1 < 0.0
|
1765
|
+
or im2 < 0.0
|
1766
|
+
or im1 * im1 > re1 - re1 * re1 + 1e-9
|
1767
|
+
or im2 * im2 > re2 - re2 * re2 + 1e-9
|
1768
|
+
):
|
1769
|
+
lifetime[0, u] = NAN
|
1770
|
+
lifetime[1, u] = NAN
|
1771
|
+
fraction[0, u] = NAN
|
1772
|
+
fraction[1, u] = NAN
|
1773
|
+
continue
|
1774
|
+
|
1775
|
+
dmin = INFINITY
|
1776
|
+
g0 = NAN
|
1777
|
+
g1 = NAN
|
1778
|
+
f = NAN
|
1779
|
+
|
1780
|
+
for i in range(candidate.shape[0]):
|
1781
|
+
# scan first component
|
1782
|
+
g0h1 = candidate[i]
|
1783
|
+
s0h1 = sqrt(g0h1 - g0h1 * g0h1)
|
1784
|
+
|
1785
|
+
# second component is intersection of semicircle with line
|
1786
|
+
# between first component and phasor coordinate
|
1787
|
+
dx = re1 - g0h1
|
1788
|
+
dy = im1 - s0h1
|
1789
|
+
dr = dx * dx + dy * dy
|
1790
|
+
dd = (g0h1 - 0.5) * im1 - (re1 - 0.5) * s0h1
|
1791
|
+
rdd = 0.25 * dr - dd * dd # discriminant
|
1792
|
+
if rdd < 0.0 or dr <= 0.0:
|
1793
|
+
# no intersection
|
1794
|
+
g0 = g0h1
|
1795
|
+
g1 = g0h1 # NAN?
|
1796
|
+
f = 1.0
|
1797
|
+
break
|
1798
|
+
rdd = sqrt(rdd)
|
1799
|
+
g0h1 = (dd * dy - copysign(1.0, dy) * dx * rdd) / dr + 0.5
|
1800
|
+
s0h1 = (-dd * dx - fabs(dy) * rdd) / dr
|
1801
|
+
g1h1 = (dd * dy + copysign(1.0, dy) * dx * rdd) / dr + 0.5
|
1802
|
+
s1h1 = (-dd * dx + fabs(dy) * rdd) / dr
|
1803
|
+
|
1804
|
+
# this check is numerically unstable if candidate=1.0
|
1805
|
+
if s0h1 < 0.0 or s1h1 < 0.0:
|
1806
|
+
# no other intersection with semicircle
|
1807
|
+
continue
|
1808
|
+
|
1809
|
+
if g0h1 < g1h1:
|
1810
|
+
t = g0h1
|
1811
|
+
g0h1 = g1h1
|
1812
|
+
g1h1 = t
|
1813
|
+
t = s0h1
|
1814
|
+
s0h1 = s1h1
|
1815
|
+
s1h1 = t
|
1816
|
+
|
1817
|
+
# second harmonic component coordinates on semicircle
|
1818
|
+
g0h2 = g0h1 / (4.0 - 3.0 * g0h1)
|
1819
|
+
s0h2 = sqrt(g0h2 - g0h2 * g0h2)
|
1820
|
+
g1h2 = g1h1 / (4.0 - 3.0 * g1h1)
|
1821
|
+
s1h2 = sqrt(g1h2 - g1h2 * g1h2)
|
1822
|
+
|
1823
|
+
# distance of phasor coordinates to line between
|
1824
|
+
# components at second harmonic
|
1825
|
+
# normalize line coordinates
|
1826
|
+
dx = g1h2 - g0h2
|
1827
|
+
dy = s1h2 - s0h2
|
1828
|
+
x = re2 - g0h2
|
1829
|
+
y = im2 - s0h2
|
1830
|
+
# square of line length
|
1831
|
+
t = dx * dx + dy * dy
|
1832
|
+
if t == 0.0:
|
1833
|
+
continue
|
1834
|
+
# projection of point on line using dot product
|
1835
|
+
t = (x * dx + y * dy) / t
|
1836
|
+
# square of distance of point to line
|
1837
|
+
dx = x - t * dx
|
1838
|
+
dy = y - t * dy
|
1839
|
+
d = dx * dx + dy * dy
|
1840
|
+
|
1841
|
+
if d < dmin:
|
1842
|
+
dmin = d
|
1843
|
+
g0 = g0h1
|
1844
|
+
g1 = g1h1
|
1845
|
+
f = t
|
1846
|
+
|
1847
|
+
lifetime[0, u] = <float_t> phasor_to_single_lifetime(g0, omega_sqr)
|
1848
|
+
lifetime[1, u] = <float_t> phasor_to_single_lifetime(g1, omega_sqr)
|
1849
|
+
fraction[0, u] = <float_t> (1.0 - f)
|
1850
|
+
fraction[1, u] = <float_t> f
|
1851
|
+
|
1852
|
+
|
1853
|
+
cdef inline double phasor_to_single_lifetime(
|
1854
|
+
const double real,
|
1855
|
+
const double omega_sqr,
|
1856
|
+
) noexcept nogil:
|
1857
|
+
"""Return single exponential lifetime from real coordinate."""
|
1858
|
+
cdef:
|
1859
|
+
double t
|
1860
|
+
|
1861
|
+
if isnan(real) or real < 0.0 or real > 1.0:
|
1862
|
+
return NAN
|
1863
|
+
t = real * omega_sqr
|
1864
|
+
return sqrt((1.0 - real) / t) if t > 0.0 else INFINITY
|
1865
|
+
|
1866
|
+
|
1867
|
+
def _nearest_neighbor_2d(
|
1868
|
+
int_t[::1] indices,
|
1869
|
+
const float_t[::1] x0,
|
1870
|
+
const float_t[::1] y0,
|
1871
|
+
const float_t[::1] x1,
|
1872
|
+
const float_t[::1] y1,
|
1873
|
+
const float_t distance_max,
|
1874
|
+
const int num_threads
|
1875
|
+
):
|
1876
|
+
"""Find nearest neighbors in 2D.
|
1877
|
+
|
1878
|
+
For each point in the first set of arrays (x0, y0) find the nearest point
|
1879
|
+
in the second set of arrays (x1, y1) and store the index of the nearest
|
1880
|
+
point in the second array in the indices array.
|
1881
|
+
If any coordinates are NaN, or the distance to the nearest point
|
1882
|
+
is larger than distance_max, the index is set to -1.
|
1883
|
+
|
1884
|
+
"""
|
1885
|
+
cdef:
|
1886
|
+
ssize_t i, j, index
|
1887
|
+
float_t x, y, dmin
|
1888
|
+
float_t distance_max_squared = distance_max * distance_max
|
1889
|
+
|
1890
|
+
if (
|
1891
|
+
indices.shape[0] != x0.shape[0]
|
1892
|
+
or x0.shape[0] != y0.shape[0]
|
1893
|
+
or x1.shape[0] != y1.shape[0]
|
1894
|
+
):
|
1895
|
+
raise ValueError('input array size mismatch')
|
1896
|
+
|
1897
|
+
with nogil, parallel(num_threads=num_threads):
|
1898
|
+
for i in prange(x0.shape[0]):
|
1899
|
+
x = x0[i]
|
1900
|
+
y = y0[i]
|
1901
|
+
if isnan(x) or isnan(y):
|
1902
|
+
indices[i] = -1
|
1903
|
+
continue
|
1904
|
+
index = -1
|
1905
|
+
dmin = INFINITY
|
1906
|
+
for j in range(x1.shape[0]):
|
1907
|
+
x = x0[i] - x1[j]
|
1908
|
+
y = y0[i] - y1[j]
|
1909
|
+
x = x * x + y * y
|
1910
|
+
if x < dmin:
|
1911
|
+
dmin = x
|
1912
|
+
index = j
|
1913
|
+
indices[i] = -1 if dmin > distance_max_squared else <int_t> index
|
1914
|
+
|
1915
|
+
|
1916
|
+
###############################################################################
|
1917
|
+
# Interpolation functions
|
1918
|
+
|
1919
|
+
|
1920
|
+
def _mean_value_coordinates(
|
1921
|
+
float_t[:, ::1] fraction, # vertices, points
|
1922
|
+
const ssize_t[::1] order,
|
1923
|
+
const float_t[::1] px, # points
|
1924
|
+
const float_t[::1] py,
|
1925
|
+
const float_t[::1] vx, # polygon vertices
|
1926
|
+
const float_t[::1] vy,
|
1927
|
+
const int num_threads
|
1928
|
+
):
|
1929
|
+
"""Calculate mean value coordinates of points in polygon.
|
1930
|
+
|
1931
|
+
https://doi.org/10.1016/j.cagd.2024.102310
|
1932
|
+
|
1933
|
+
"""
|
1934
|
+
cdef:
|
1935
|
+
ssize_t i, j, k, p, nv
|
1936
|
+
double x, y, alpha, weight, weight_sum
|
1937
|
+
double* weights = NULL
|
1938
|
+
double* sigma = NULL
|
1939
|
+
double* length = NULL
|
1940
|
+
|
1941
|
+
if px.shape[0] != py.shape[0]:
|
1942
|
+
raise ValueError('px and py shape mismatch')
|
1943
|
+
if vx.shape[0] != vy.shape[0]:
|
1944
|
+
raise ValueError('vx and vy shape mismatch')
|
1945
|
+
if fraction.shape[0] != vx.shape[0] or fraction.shape[1] != px.shape[0]:
|
1946
|
+
raise ValueError('fraction, vx or px shape mismatch')
|
1947
|
+
if fraction.shape[0] != order.shape[0]:
|
1948
|
+
raise ValueError('fraction and order shape mismatch')
|
1949
|
+
if fraction.shape[0] < 3:
|
1950
|
+
raise ValueError('not a polygon')
|
1951
|
+
|
1952
|
+
nv = fraction.shape[0]
|
1953
|
+
|
1954
|
+
with nogil, parallel(num_threads=num_threads):
|
1955
|
+
weights = <double *> malloc(3 * nv * sizeof(double))
|
1956
|
+
if weights == NULL:
|
1957
|
+
with gil:
|
1958
|
+
raise MemoryError('failed to allocate thread-local buffer')
|
1959
|
+
sigma = &weights[nv]
|
1960
|
+
length = &weights[nv * 2]
|
1961
|
+
|
1962
|
+
for p in prange(px.shape[0]):
|
1963
|
+
x = px[p]
|
1964
|
+
y = py[p]
|
1965
|
+
|
1966
|
+
if isnan(x) or isnan(y):
|
1967
|
+
for i in range(nv):
|
1968
|
+
fraction[i, p] = <float_t> NAN
|
1969
|
+
continue
|
1970
|
+
|
1971
|
+
for i in range(nv):
|
1972
|
+
j = (i + 1) % nv # next vertex, wrapped around
|
1973
|
+
sigma[i] = (
|
1974
|
+
angle(vx[j], vy[j], vx[i], vy[i], x, y) # beta
|
1975
|
+
- angle(vx[i], vy[i], vx[j], vy[j], x, y) # gamma
|
1976
|
+
)
|
1977
|
+
length[i] = hypot(vx[i] - x, vy[i] - y)
|
1978
|
+
|
1979
|
+
weight_sum = 0.0
|
1980
|
+
for i in range(nv):
|
1981
|
+
j = (i + 1) % nv # next vertex, wrapped around
|
1982
|
+
k = (i - 1 + nv) % nv # previous vertex, wrapped around
|
1983
|
+
|
1984
|
+
alpha = angle(vx[k], vy[k], x, y, vx[j], vy[j])
|
1985
|
+
if sign(alpha) != sign(
|
1986
|
+
M_PI * (sign(sigma[k]) + sign(sigma[i]))
|
1987
|
+
- sigma[k] - sigma[i]
|
1988
|
+
):
|
1989
|
+
alpha = -alpha
|
1990
|
+
weight = length[k] * sin(alpha * 0.5)
|
1991
|
+
for j in range(nv):
|
1992
|
+
if j != k and j != i:
|
1993
|
+
weight = weight * length[j] * sin(fabs(sigma[j]) * 0.5)
|
1994
|
+
weight_sum = weight_sum + weight
|
1995
|
+
weights[i] = weight
|
1996
|
+
|
1997
|
+
if fabs(weight_sum) > 1e-12:
|
1998
|
+
for i in range(nv):
|
1999
|
+
fraction[order[i], p] = <float_t> (weights[i] / weight_sum)
|
2000
|
+
else:
|
2001
|
+
for i in range(nv):
|
2002
|
+
fraction[i, p] = <float_t> NAN
|
2003
|
+
|
2004
|
+
free(weights)
|
2005
|
+
|
2006
|
+
|
2007
|
+
cdef inline int sign(const double x) noexcept nogil:
|
2008
|
+
"""Return sign of x."""
|
2009
|
+
return 0 if fabs(x) < 1e-12 else (1 if x > 0.0 else -1)
|
2010
|
+
|
2011
|
+
|
2012
|
+
cdef inline double angle(
|
2013
|
+
const double x0,
|
2014
|
+
const double y0,
|
2015
|
+
const double x1,
|
2016
|
+
const double y1,
|
2017
|
+
const double x2,
|
2018
|
+
const double y2,
|
2019
|
+
) noexcept nogil:
|
2020
|
+
"""Return angle at (x1, y1)."""
|
2021
|
+
cdef:
|
2022
|
+
double ax = x0 - x1
|
2023
|
+
double ay = y0 - y1
|
2024
|
+
double bx = x2 - x1
|
2025
|
+
double by = y2 - y1
|
2026
|
+
|
2027
|
+
return atan2(ax * by - ay * bx, ax * bx + ay * by)
|
2028
|
+
|
2029
|
+
|
2030
|
+
###############################################################################
|
2031
|
+
# Blend ufuncs
|
2032
|
+
|
2033
|
+
|
2034
|
+
@cython.ufunc
|
2035
|
+
cdef float_t _blend_and(
|
2036
|
+
float_t a, # base layer
|
2037
|
+
float_t b, # blend layer
|
2038
|
+
) noexcept nogil:
|
2039
|
+
"""Return blended layers using `and` mode."""
|
2040
|
+
if isnan(a):
|
2041
|
+
return NAN
|
2042
|
+
return b
|
2043
|
+
|
2044
|
+
|
2045
|
+
@cython.ufunc
|
2046
|
+
cdef float_t _blend_normal(
|
2047
|
+
float_t a, # base layer
|
2048
|
+
float_t b, # blend layer
|
2049
|
+
) noexcept nogil:
|
2050
|
+
"""Return blended layers using `normal` mode."""
|
2051
|
+
if isnan(b):
|
2052
|
+
return a
|
2053
|
+
return b
|
2054
|
+
|
2055
|
+
|
2056
|
+
@cython.ufunc
|
2057
|
+
cdef float_t _blend_multiply(
|
2058
|
+
float_t a, # base layer
|
2059
|
+
float_t b, # blend layer
|
2060
|
+
) noexcept nogil:
|
2061
|
+
"""Return blended layers using `multiply` mode."""
|
2062
|
+
if isnan(b):
|
2063
|
+
return a
|
2064
|
+
return a * b
|
2065
|
+
|
2066
|
+
|
2067
|
+
@cython.ufunc
|
2068
|
+
cdef float_t _blend_screen(
|
2069
|
+
float_t a, # base layer
|
2070
|
+
float_t b, # blend layer
|
2071
|
+
) noexcept nogil:
|
2072
|
+
"""Return blended layers using `screen` mode."""
|
2073
|
+
if isnan(b):
|
2074
|
+
return a
|
2075
|
+
return <float_t> (1.0 - (1.0 - a) * (1.0 - b))
|
2076
|
+
|
2077
|
+
|
2078
|
+
@cython.ufunc
|
2079
|
+
cdef float_t _blend_overlay(
|
2080
|
+
float_t a, # base layer
|
2081
|
+
float_t b, # blend layer
|
2082
|
+
) noexcept nogil:
|
2083
|
+
"""Return blended layers using `overlay` mode."""
|
2084
|
+
if isnan(b) or isnan(a):
|
2085
|
+
return a
|
2086
|
+
if a < 0.5:
|
2087
|
+
return <float_t> (2.0 * a * b)
|
2088
|
+
return <float_t> (1.0 - 2.0 * (1.0 - a) * (1.0 - b))
|
2089
|
+
|
2090
|
+
|
2091
|
+
@cython.ufunc
|
2092
|
+
cdef float_t _blend_darken(
|
2093
|
+
float_t a, # base layer
|
2094
|
+
float_t b, # blend layer
|
2095
|
+
) noexcept nogil:
|
2096
|
+
"""Return blended layers using `darken` mode."""
|
2097
|
+
if isnan(b) or isnan(a):
|
2098
|
+
return a
|
2099
|
+
return <float_t> min(a, b)
|
2100
|
+
|
2101
|
+
|
2102
|
+
@cython.ufunc
|
2103
|
+
cdef float_t _blend_lighten(
|
2104
|
+
float_t a, # base layer
|
2105
|
+
float_t b, # blend layer
|
2106
|
+
) noexcept nogil:
|
2107
|
+
"""Return blended layers using `lighten` mode."""
|
2108
|
+
if isnan(b) or isnan(a):
|
2109
|
+
return a
|
2110
|
+
return <float_t> max(a, b)
|
2111
|
+
|
2112
|
+
|
2113
|
+
###############################################################################
|
2114
|
+
# Threshold ufuncs
|
2115
|
+
|
2116
|
+
|
2117
|
+
@cython.ufunc
|
2118
|
+
cdef (float_t, float_t, float_t) _phasor_threshold_open(
|
2119
|
+
float_t mean,
|
2120
|
+
float_t real,
|
2121
|
+
float_t imag,
|
2122
|
+
float_t mean_min,
|
2123
|
+
float_t mean_max,
|
2124
|
+
float_t real_min,
|
2125
|
+
float_t real_max,
|
2126
|
+
float_t imag_min,
|
2127
|
+
float_t imag_max,
|
2128
|
+
float_t phase_min,
|
2129
|
+
float_t phase_max,
|
2130
|
+
float_t modulation_min,
|
2131
|
+
float_t modulation_max,
|
2132
|
+
) noexcept nogil:
|
2133
|
+
"""Return thresholded values by open intervals."""
|
2134
|
+
cdef:
|
2135
|
+
double phi = NAN
|
2136
|
+
double mod = NAN
|
2137
|
+
|
2138
|
+
if isnan(mean) or isnan(real) or isnan(imag):
|
2139
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2140
|
+
|
2141
|
+
if not isnan(mean_min) and mean <= mean_min:
|
2142
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2143
|
+
if not isnan(mean_max) and mean >= mean_max:
|
2144
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2145
|
+
|
2146
|
+
if not isnan(real_min) and real <= real_min:
|
2147
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2148
|
+
if not isnan(real_max) and real >= real_max:
|
2149
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2150
|
+
|
2151
|
+
if not isnan(imag_min) and imag <= imag_min:
|
2152
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2153
|
+
if not isnan(imag_max) and imag >= imag_max:
|
2154
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2155
|
+
|
2156
|
+
if not isnan(modulation_min):
|
2157
|
+
mod = real * real + imag * imag
|
2158
|
+
if mod <= modulation_min * modulation_min:
|
2159
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2160
|
+
if not isnan(modulation_max):
|
2161
|
+
if isnan(mod):
|
2162
|
+
mod = real * real + imag * imag
|
2163
|
+
if mod >= modulation_max * modulation_max:
|
2164
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2165
|
+
|
2166
|
+
if not isnan(phase_min):
|
2167
|
+
phi = atan2(imag, real)
|
2168
|
+
if phi <= phase_min:
|
2169
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2170
|
+
if not isnan(phase_max):
|
2171
|
+
if isnan(phi):
|
2172
|
+
phi = atan2(imag, real)
|
2173
|
+
if phi >= phase_max:
|
2174
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2175
|
+
|
2176
|
+
return mean, real, imag
|
2177
|
+
|
2178
|
+
|
2179
|
+
@cython.ufunc
|
2180
|
+
cdef (float_t, float_t, float_t) _phasor_threshold_closed(
|
2181
|
+
float_t mean,
|
2182
|
+
float_t real,
|
2183
|
+
float_t imag,
|
2184
|
+
float_t mean_min,
|
2185
|
+
float_t mean_max,
|
2186
|
+
float_t real_min,
|
2187
|
+
float_t real_max,
|
2188
|
+
float_t imag_min,
|
2189
|
+
float_t imag_max,
|
2190
|
+
float_t phase_min,
|
2191
|
+
float_t phase_max,
|
2192
|
+
float_t modulation_min,
|
2193
|
+
float_t modulation_max,
|
2194
|
+
) noexcept nogil:
|
2195
|
+
"""Return thresholded values by closed intervals."""
|
2196
|
+
cdef:
|
2197
|
+
double phi = NAN
|
2198
|
+
double mod = NAN
|
2199
|
+
|
2200
|
+
if isnan(mean) or isnan(real) or isnan(imag):
|
2201
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2202
|
+
|
2203
|
+
if not isnan(mean_min) and mean < mean_min:
|
2204
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2205
|
+
if not isnan(mean_max) and mean > mean_max:
|
2206
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2207
|
+
|
2208
|
+
if not isnan(real_min) and real < real_min:
|
2209
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2210
|
+
if not isnan(real_max) and real > real_max:
|
2211
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2212
|
+
|
2213
|
+
if not isnan(imag_min) and imag < imag_min:
|
2214
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2215
|
+
if not isnan(imag_max) and imag > imag_max:
|
2216
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2217
|
+
|
2218
|
+
if not isnan(modulation_min):
|
2219
|
+
mod = real * real + imag * imag
|
2220
|
+
if mod < modulation_min * modulation_min:
|
2221
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2222
|
+
if not isnan(modulation_max):
|
2223
|
+
if isnan(mod):
|
2224
|
+
mod = real * real + imag * imag
|
2225
|
+
if mod > modulation_max * modulation_max:
|
2226
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2227
|
+
|
2228
|
+
if not isnan(phase_min):
|
2229
|
+
phi = atan2(imag, real)
|
2230
|
+
if phi < phase_min:
|
2231
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2232
|
+
if not isnan(phase_max):
|
2233
|
+
if isnan(phi):
|
2234
|
+
phi = atan2(imag, real)
|
2235
|
+
if phi > phase_max:
|
2236
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2237
|
+
|
2238
|
+
return mean, real, imag
|
2239
|
+
|
2240
|
+
|
2241
|
+
@cython.ufunc
|
2242
|
+
cdef (float_t, float_t, float_t) _phasor_threshold_mean_open(
|
2243
|
+
float_t mean,
|
2244
|
+
float_t real,
|
2245
|
+
float_t imag,
|
2246
|
+
float_t mean_min,
|
2247
|
+
float_t mean_max,
|
2248
|
+
) noexcept nogil:
|
2249
|
+
"""Return thresholded values only by open interval of `mean`."""
|
2250
|
+
if isnan(mean) or isnan(real) or isnan(imag):
|
2251
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2252
|
+
|
2253
|
+
if not isnan(mean_min) and mean <= mean_min:
|
2254
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2255
|
+
if not isnan(mean_max) and mean >= mean_max:
|
2256
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2257
|
+
|
2258
|
+
return mean, real, imag
|
2259
|
+
|
2260
|
+
|
2261
|
+
@cython.ufunc
|
2262
|
+
cdef (float_t, float_t, float_t) _phasor_threshold_mean_closed(
|
2263
|
+
float_t mean,
|
2264
|
+
float_t real,
|
2265
|
+
float_t imag,
|
2266
|
+
float_t mean_min,
|
2267
|
+
float_t mean_max,
|
2268
|
+
) noexcept nogil:
|
2269
|
+
"""Return thresholded values only by closed interval of `mean`."""
|
2270
|
+
if isnan(mean) or isnan(real) or isnan(imag):
|
2271
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2272
|
+
|
2273
|
+
if not isnan(mean_min) and mean < mean_min:
|
2274
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2275
|
+
if not isnan(mean_max) and mean > mean_max:
|
2276
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2277
|
+
|
2278
|
+
return mean, real, imag
|
2279
|
+
|
2280
|
+
|
2281
|
+
@cython.ufunc
|
2282
|
+
cdef (float_t, float_t, float_t) _phasor_threshold_nan(
|
2283
|
+
float_t mean,
|
2284
|
+
float_t real,
|
2285
|
+
float_t imag,
|
2286
|
+
) noexcept nogil:
|
2287
|
+
"""Return the input values if any of them is not NaN."""
|
2288
|
+
if isnan(mean) or isnan(real) or isnan(imag):
|
2289
|
+
return <float_t> NAN, <float_t> NAN, <float_t> NAN
|
2290
|
+
|
2291
|
+
return mean, real, imag
|
2292
|
+
|
2293
|
+
|
2294
|
+
###############################################################################
|
2295
|
+
# Unary ufuncs
|
2296
|
+
|
2297
|
+
|
2298
|
+
@cython.ufunc
|
2299
|
+
cdef float_t _anscombe(
|
2300
|
+
float_t x,
|
2301
|
+
) noexcept nogil:
|
2302
|
+
"""Return anscombe variance stabilizing transformation."""
|
2303
|
+
if isnan(x):
|
2304
|
+
return <float_t> NAN
|
2305
|
+
|
2306
|
+
return <float_t> (2.0 * sqrt(<double> x + 0.375))
|
2307
|
+
|
2308
|
+
|
2309
|
+
@cython.ufunc
|
2310
|
+
cdef float_t _anscombe_inverse(
|
2311
|
+
float_t x,
|
2312
|
+
) noexcept nogil:
|
2313
|
+
"""Return inverse anscombe transformation."""
|
2314
|
+
if isnan(x):
|
2315
|
+
return <float_t> NAN
|
2316
|
+
|
2317
|
+
return <float_t> (x * x / 4.0 - 0.375) # 3/8
|
2318
|
+
|
2319
|
+
|
2320
|
+
@cython.ufunc
|
2321
|
+
cdef float_t _anscombe_inverse_approx(
|
2322
|
+
float_t x,
|
2323
|
+
) noexcept nogil:
|
2324
|
+
"""Return inverse anscombe transformation.
|
2325
|
+
|
2326
|
+
Using approximation of exact unbiased inverse.
|
2327
|
+
|
2328
|
+
"""
|
2329
|
+
if isnan(x):
|
2330
|
+
return <float_t> NAN
|
2331
|
+
|
2332
|
+
return <float_t> (
|
2333
|
+
0.25 * x * x # 1/4
|
2334
|
+
+ 0.30618621784789724 / x # 1/4 * sqrt(3/2)
|
2335
|
+
- 1.375 / (x * x) # 11/8
|
2336
|
+
+ 0.7654655446197431 / (x * x * x) # 5/8 * sqrt(3/2)
|
2337
|
+
- 0.125 # 1/8
|
2338
|
+
)
|
2339
|
+
|
2340
|
+
|
2341
|
+
###############################################################################
|
2342
|
+
# Denoising in spectral space
|
2343
|
+
|
2344
|
+
|
2345
|
+
def _phasor_from_signal_vector(
|
2346
|
+
float_t[:, ::1] phasor,
|
2347
|
+
const signal_t[:, ::1] signal,
|
2348
|
+
const double[:, :, ::1] sincos,
|
2349
|
+
const int num_threads
|
2350
|
+
):
|
2351
|
+
"""Calculate phasor coordinate vectors from signal along last axis.
|
2352
|
+
|
2353
|
+
Parameters
|
2354
|
+
----------
|
2355
|
+
phasor : 2D memoryview of float32 or float64
|
2356
|
+
Writable buffer of two dimensions where calculated phasor
|
2357
|
+
vectors are stored:
|
2358
|
+
|
2359
|
+
0. other dimensions flat
|
2360
|
+
1. real and imaginary components
|
2361
|
+
|
2362
|
+
signal : 2D memoryview of float32 or float64
|
2363
|
+
Buffer of two dimensions containing signal:
|
2364
|
+
|
2365
|
+
0. other dimensions flat
|
2366
|
+
1. dimension over which to compute FFT, number samples
|
2367
|
+
|
2368
|
+
sincos : 3D memoryview of float64
|
2369
|
+
Buffer of three dimensions containing sine and cosine terms to be
|
2370
|
+
multiplied with signal:
|
2371
|
+
|
2372
|
+
0. number harmonics
|
2373
|
+
1. number samples
|
2374
|
+
2. cos and sin
|
2375
|
+
|
2376
|
+
num_threads : int
|
2377
|
+
Number of OpenMP threads to use for parallelization.
|
2378
|
+
|
2379
|
+
Notes
|
2380
|
+
-----
|
2381
|
+
This implementation requires contiguous input arrays.
|
2382
|
+
|
2383
|
+
"""
|
2384
|
+
cdef:
|
2385
|
+
ssize_t size = signal.shape[0]
|
2386
|
+
ssize_t samples = signal.shape[1]
|
2387
|
+
ssize_t harmonics = sincos.shape[0]
|
2388
|
+
ssize_t i, j, k, h
|
2389
|
+
double dc, re, im, sample
|
2390
|
+
|
2391
|
+
if (
|
2392
|
+
samples < 2
|
2393
|
+
or harmonics > samples // 2
|
2394
|
+
or phasor.shape[0] != size
|
2395
|
+
or phasor.shape[1] != harmonics * 2
|
2396
|
+
):
|
2397
|
+
raise ValueError('invalid shape of phasor or signal')
|
2398
|
+
if sincos.shape[1] != samples or sincos.shape[2] != 2:
|
2399
|
+
raise ValueError('invalid shape of sincos')
|
2400
|
+
|
2401
|
+
with nogil, parallel(num_threads=num_threads):
|
2402
|
+
for i in prange(signal.shape[0]):
|
2403
|
+
j = 0
|
2404
|
+
for h in range(harmonics):
|
2405
|
+
dc = 0.0
|
2406
|
+
re = 0.0
|
2407
|
+
im = 0.0
|
2408
|
+
for k in range(samples):
|
2409
|
+
sample = <double> signal[i, k]
|
2410
|
+
dc = dc + sample
|
2411
|
+
re = re + sample * sincos[h, k, 0]
|
2412
|
+
im = im + sample * sincos[h, k, 1]
|
2413
|
+
if dc != 0.0:
|
2414
|
+
re = re / dc
|
2415
|
+
im = im / dc
|
2416
|
+
else:
|
2417
|
+
re = NAN if re == 0.0 else re * INFINITY
|
2418
|
+
im = NAN if im == 0.0 else im * INFINITY
|
2419
|
+
phasor[i, j] = <float_t> re
|
2420
|
+
j = j + 1
|
2421
|
+
phasor[i, j] = <float_t> im
|
2422
|
+
j = j + 1
|
2423
|
+
|
2424
|
+
|
2425
|
+
def _signal_denoise_vector(
|
2426
|
+
float_t[:, ::1] denoised,
|
2427
|
+
float_t[::1] integrated,
|
2428
|
+
const signal_t[:, ::1] signal,
|
2429
|
+
const float_t[:, ::1] spectral_vector,
|
2430
|
+
const double sigma,
|
2431
|
+
const double vmin,
|
2432
|
+
const int num_threads
|
2433
|
+
):
|
2434
|
+
"""Calculate denoised signal from spectral_vector."""
|
2435
|
+
cdef:
|
2436
|
+
ssize_t size = signal.shape[0]
|
2437
|
+
ssize_t samples = signal.shape[1]
|
2438
|
+
ssize_t dims = spectral_vector.shape[1]
|
2439
|
+
ssize_t i, j, m
|
2440
|
+
float_t n
|
2441
|
+
double weight, sum, t
|
2442
|
+
double sigma2 = -1.0 / (2.0 * sigma * sigma)
|
2443
|
+
double threshold = 9.0 * sigma * sigma
|
2444
|
+
|
2445
|
+
if denoised.shape[0] != size or denoised.shape[1] != samples:
|
2446
|
+
raise ValueError('signal and denoised shape mismatch')
|
2447
|
+
if integrated.shape[0] != size:
|
2448
|
+
raise ValueError('integrated.shape[0] != signal.shape[0]')
|
2449
|
+
if spectral_vector.shape[0] != size:
|
2450
|
+
raise ValueError('spectral_vector.shape[0] != signal.shape[0]')
|
2451
|
+
|
2452
|
+
with nogil, parallel(num_threads=num_threads):
|
2453
|
+
|
2454
|
+
# integrate channel intensities for each pixel
|
2455
|
+
# and filter low intensities
|
2456
|
+
for i in prange(size):
|
2457
|
+
sum = 0.0
|
2458
|
+
for m in range(samples):
|
2459
|
+
sum = sum + <double> signal[i, m]
|
2460
|
+
if sum < vmin:
|
2461
|
+
sum = NAN
|
2462
|
+
integrated[i] = <float_t> sum
|
2463
|
+
|
2464
|
+
# loop over all pixels
|
2465
|
+
for i in prange(size):
|
2466
|
+
|
2467
|
+
n = integrated[i]
|
2468
|
+
if not n > 0.0:
|
2469
|
+
# n is NaN or zero; cannot denoise; return original signal
|
2470
|
+
continue
|
2471
|
+
|
2472
|
+
for m in range(samples):
|
2473
|
+
denoised[i, m] /= n # weight = 1.0
|
2474
|
+
|
2475
|
+
# loop over other pixels
|
2476
|
+
for j in range(size):
|
2477
|
+
if i == j:
|
2478
|
+
# weight = 1.0 already accounted for
|
2479
|
+
continue
|
2480
|
+
|
2481
|
+
n = integrated[j]
|
2482
|
+
if not n > 0.0:
|
2483
|
+
# n is NaN or zero
|
2484
|
+
continue
|
2485
|
+
|
2486
|
+
# calculate weight from Euclidean distance of
|
2487
|
+
# pixels i and j in spectral vector space
|
2488
|
+
sum = 0.0
|
2489
|
+
for m in range(dims):
|
2490
|
+
t = spectral_vector[i, m] - spectral_vector[j, m]
|
2491
|
+
sum = sum + t * t
|
2492
|
+
if sum > threshold:
|
2493
|
+
sum = -1.0
|
2494
|
+
break
|
2495
|
+
if sum >= 0.0:
|
2496
|
+
weight = exp(sum * sigma2) / n
|
2497
|
+
else:
|
2498
|
+
# sum is NaN or greater than threshold
|
2499
|
+
continue
|
2500
|
+
|
2501
|
+
# add weighted signal[j] to denoised[i]
|
2502
|
+
for m in range(samples):
|
2503
|
+
denoised[i, m] += <float_t> (weight * signal[j, m])
|
2504
|
+
|
2505
|
+
# re-normalize to original intensity
|
2506
|
+
# sum cannot be zero because integrated == 0 was filtered
|
2507
|
+
sum = 0.0
|
2508
|
+
for m in range(samples):
|
2509
|
+
sum = sum + denoised[i, m]
|
2510
|
+
n = <float_t> (<double> integrated[i] / sum)
|
2511
|
+
for m in range(samples):
|
2512
|
+
denoised[i, m] *= n
|
2513
|
+
|
2514
|
+
|
2515
|
+
###############################################################################
|
2516
|
+
# Filtering functions
|
2517
|
+
|
2518
|
+
|
2519
|
+
cdef float_t _median(float_t *values, const ssize_t size) noexcept nogil:
|
2520
|
+
"""Return median of array values using Quickselect algorithm."""
|
2521
|
+
cdef:
|
2522
|
+
ssize_t i, pivot_index, pivot_index_new
|
2523
|
+
ssize_t left = 0
|
2524
|
+
ssize_t right = size - 1
|
2525
|
+
ssize_t middle = size // 2
|
2526
|
+
float_t pivot_value, temp
|
2527
|
+
|
2528
|
+
if size % 2 == 0:
|
2529
|
+
middle -= 1 # Quickselect sorts on right
|
2530
|
+
|
2531
|
+
while left <= right:
|
2532
|
+
pivot_index = left + (right - left) // 2
|
2533
|
+
pivot_value = values[pivot_index]
|
2534
|
+
temp = values[pivot_index]
|
2535
|
+
values[pivot_index] = values[right]
|
2536
|
+
values[right] = temp
|
2537
|
+
pivot_index_new = left
|
2538
|
+
for i in range(left, right):
|
2539
|
+
if values[i] < pivot_value:
|
2540
|
+
temp = values[i]
|
2541
|
+
values[i] = values[pivot_index_new]
|
2542
|
+
values[pivot_index_new] = temp
|
2543
|
+
pivot_index_new += 1
|
2544
|
+
temp = values[right]
|
2545
|
+
values[right] = values[pivot_index_new]
|
2546
|
+
values[pivot_index_new] = temp
|
2547
|
+
|
2548
|
+
if pivot_index_new == middle:
|
2549
|
+
if size % 2 == 0:
|
2550
|
+
return (values[middle] + values[middle + 1]) / <float_t> 2.0
|
2551
|
+
return values[middle]
|
2552
|
+
if pivot_index_new < middle:
|
2553
|
+
left = pivot_index_new + 1
|
2554
|
+
else:
|
2555
|
+
right = pivot_index_new - 1
|
2556
|
+
|
2557
|
+
return values[middle] # unreachable code?
|
2558
|
+
|
2559
|
+
|
2560
|
+
def _median_filter_2d(
|
2561
|
+
float_t[:, :] image,
|
2562
|
+
float_t[:, ::1] filtered_image,
|
2563
|
+
const ssize_t kernel_size,
|
2564
|
+
const int repeat=1,
|
2565
|
+
const int num_threads=1,
|
2566
|
+
):
|
2567
|
+
"""Apply 2D median filter ignoring NaN."""
|
2568
|
+
cdef:
|
2569
|
+
ssize_t rows = image.shape[0]
|
2570
|
+
ssize_t cols = image.shape[1]
|
2571
|
+
ssize_t k = kernel_size // 2
|
2572
|
+
ssize_t i, j, r, di, dj, ki, kj, valid_count
|
2573
|
+
float_t element
|
2574
|
+
float_t *kernel
|
2575
|
+
|
2576
|
+
if kernel_size <= 0:
|
2577
|
+
raise ValueError('kernel_size must be greater than 0')
|
2578
|
+
|
2579
|
+
with nogil, parallel(num_threads=num_threads):
|
2580
|
+
|
2581
|
+
kernel = <float_t *> malloc(
|
2582
|
+
kernel_size * kernel_size * sizeof(float_t)
|
2583
|
+
)
|
2584
|
+
if kernel == NULL:
|
2585
|
+
with gil:
|
2586
|
+
raise MemoryError('failed to allocate kernel')
|
2587
|
+
|
2588
|
+
for r in range(repeat):
|
2589
|
+
for i in prange(rows):
|
2590
|
+
for j in range(cols):
|
2591
|
+
if isnan(image[i, j]):
|
2592
|
+
filtered_image[i, j] = <float_t> NAN
|
2593
|
+
continue
|
2594
|
+
valid_count = 0
|
2595
|
+
for di in range(kernel_size):
|
2596
|
+
ki = i - k + di
|
2597
|
+
if ki < 0:
|
2598
|
+
ki = 0
|
2599
|
+
elif ki >= rows:
|
2600
|
+
ki = rows - 1
|
2601
|
+
for dj in range(kernel_size):
|
2602
|
+
kj = j - k + dj
|
2603
|
+
if kj < 0:
|
2604
|
+
kj = 0
|
2605
|
+
elif kj >= cols:
|
2606
|
+
kj = cols - 1
|
2607
|
+
element = image[ki, kj]
|
2608
|
+
if not isnan(element):
|
2609
|
+
kernel[valid_count] = element
|
2610
|
+
valid_count = valid_count + 1
|
2611
|
+
filtered_image[i, j] = _median(kernel, valid_count)
|
2612
|
+
|
2613
|
+
for i in prange(rows):
|
2614
|
+
for j in range(cols):
|
2615
|
+
image[i, j] = filtered_image[i, j]
|
2616
|
+
|
2617
|
+
free(kernel)
|
2618
|
+
|
2619
|
+
|
2620
|
+
###############################################################################
|
2621
|
+
# Decoder functions
|
2622
|
+
|
2623
|
+
|
2624
|
+
@cython.boundscheck(True)
|
2625
|
+
def _flimlabs_signal(
|
2626
|
+
uint_t[:, :, ::] signal, # channel, pixel, bin
|
2627
|
+
list data, # list[list[list[[int, int]]]]
|
2628
|
+
ssize_t channel = -1 # -1 == None
|
2629
|
+
):
|
2630
|
+
"""Return TCSPC histogram image from FLIM LABS JSON intensity data."""
|
2631
|
+
cdef:
|
2632
|
+
uint_t[::] signal_
|
2633
|
+
list channels, pixels
|
2634
|
+
ssize_t c, i, h, count
|
2635
|
+
|
2636
|
+
if channel < 0:
|
2637
|
+
c = 0
|
2638
|
+
for channels in data:
|
2639
|
+
i = 0
|
2640
|
+
for pixels in channels:
|
2641
|
+
signal_ = signal[c, i]
|
2642
|
+
for h, count in pixels:
|
2643
|
+
signal_[h] = <uint_t> count
|
2644
|
+
i += 1
|
2645
|
+
c += 1
|
2646
|
+
else:
|
2647
|
+
i = 0
|
2648
|
+
for pixels in data[channel]:
|
2649
|
+
signal_ = signal[0, i]
|
2650
|
+
for h, count in pixels:
|
2651
|
+
signal_[h] = <uint_t> count
|
2652
|
+
i += 1
|
2653
|
+
|
2654
|
+
|
2655
|
+
@cython.boundscheck(True)
|
2656
|
+
def _flimlabs_mean(
|
2657
|
+
float_t[:, ::] mean, # channel, pixel
|
2658
|
+
list data, # list[list[list[[int, int]]]]
|
2659
|
+
ssize_t channel = -1 # -1 == None
|
2660
|
+
):
|
2661
|
+
"""Return mean intensity image from FLIM LABS JSON intensity data."""
|
2662
|
+
cdef:
|
2663
|
+
float_t[::] mean_
|
2664
|
+
list channels, pixels
|
2665
|
+
ssize_t c, i, h, count
|
2666
|
+
double sum
|
2667
|
+
|
2668
|
+
if channel < 0:
|
2669
|
+
c = 0
|
2670
|
+
for channels in data:
|
2671
|
+
mean_ = mean[c]
|
2672
|
+
i = 0
|
2673
|
+
for pixels in channels:
|
2674
|
+
sum = 0.0
|
2675
|
+
for h, count in pixels:
|
2676
|
+
sum += <double> count
|
2677
|
+
mean_[i] = <float_t> (sum / 256.0)
|
2678
|
+
i += 1
|
2679
|
+
c += 1
|
2680
|
+
else:
|
2681
|
+
i = 0
|
2682
|
+
mean_ = mean[0]
|
2683
|
+
for pixels in data[channel]:
|
2684
|
+
sum = 0.0
|
2685
|
+
for h, count in pixels:
|
2686
|
+
sum += <double> count
|
2687
|
+
mean_[i] = <float_t> (sum / 256.0)
|
2688
|
+
i += 1
|