phasorpy 0.7__cp314-cp314-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/_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