Skip to content

Commit dca58a1

Browse files
committed
1 parent b66a7b1 commit dca58a1

402 files changed

Lines changed: 115204 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/v0.9/.buildinfo

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Sphinx build info version 1
2+
# This file records the configuration used when building these files. When it is not found, a full rebuild will be done.
3+
config: 9e92ae3d793ac33fcfacde70503720ea
4+
tags: d77d1c0d9ca2f4c8421862c7c5a0d620
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"\n# Cursors\n\nAn introduction to selecting phasor coordinates using cursors.\n"
8+
]
9+
},
10+
{
11+
"cell_type": "markdown",
12+
"metadata": {},
13+
"source": [
14+
"Import required modules, functions, and classes:\n\n"
15+
]
16+
},
17+
{
18+
"cell_type": "code",
19+
"execution_count": null,
20+
"metadata": {
21+
"collapsed": false
22+
},
23+
"outputs": [],
24+
"source": [
25+
"from phasorpy.color import CATEGORICAL\nfrom phasorpy.cursor import (\n mask_from_circular_cursor,\n mask_from_elliptic_cursor,\n mask_from_polar_cursor,\n pseudo_color,\n)\nfrom phasorpy.datasets import fetch\nfrom phasorpy.filter import phasor_threshold\nfrom phasorpy.io import signal_from_lsm\nfrom phasorpy.phasor import phasor_from_signal\nfrom phasorpy.plot import PhasorPlot, plot_image"
26+
]
27+
},
28+
{
29+
"cell_type": "markdown",
30+
"metadata": {},
31+
"source": [
32+
"Load a hyperspectral dataset used throughout this tutorial:\n\n"
33+
]
34+
},
35+
{
36+
"cell_type": "code",
37+
"execution_count": null,
38+
"metadata": {
39+
"collapsed": false
40+
},
41+
"outputs": [],
42+
"source": [
43+
"signal = signal_from_lsm(fetch('paramecium.lsm'))\nmean, real, imag = phasor_from_signal(signal, axis=0)\n\n# remove coordinates with zero intensity\nmean_thresholded, real, imag = phasor_threshold(mean, real, imag, mean_min=1)"
44+
]
45+
},
46+
{
47+
"cell_type": "markdown",
48+
"metadata": {},
49+
"source": [
50+
"## Circular cursors\n\nUse circular cursors to mask regions of interest in the phasor space.\nDefine two cursors by specifying their real and imaginary coordinates\nand radii:\n\n"
51+
]
52+
},
53+
{
54+
"cell_type": "code",
55+
"execution_count": null,
56+
"metadata": {
57+
"collapsed": false
58+
},
59+
"outputs": [],
60+
"source": [
61+
"cursor_real = [-0.33, 0.54]\ncursor_imag = [-0.72, -0.74]\nradius = [0.2, 0.22]\n\ncircular_mask = mask_from_circular_cursor(\n real, imag, cursor_real, cursor_imag, radius=radius\n)"
62+
]
63+
},
64+
{
65+
"cell_type": "markdown",
66+
"metadata": {},
67+
"source": [
68+
"Show the circular cursors in a phasor plot:\n\n"
69+
]
70+
},
71+
{
72+
"cell_type": "code",
73+
"execution_count": null,
74+
"metadata": {
75+
"collapsed": false
76+
},
77+
"outputs": [],
78+
"source": [
79+
"plot = PhasorPlot(allquadrants=True, title='Circular cursors')\nplot.hist2d(real, imag, cmap='Greys')\nplot.cursor(\n cursor_real,\n cursor_imag,\n radius=radius,\n color=CATEGORICAL[:2],\n label=['cursor 0', 'cursor 1'],\n)\nplot.show()"
80+
]
81+
},
82+
{
83+
"cell_type": "markdown",
84+
"metadata": {},
85+
"source": [
86+
"The cursor masks can be blended to produce a pseudo-colored image.\nEach cursor's region is assigned a different color:\n\n"
87+
]
88+
},
89+
{
90+
"cell_type": "code",
91+
"execution_count": null,
92+
"metadata": {
93+
"collapsed": false
94+
},
95+
"outputs": [],
96+
"source": [
97+
"pseudo_color_image = pseudo_color(*circular_mask)\n\nplot_image(\n pseudo_color_image, title='Pseudo-color image from circular cursors'\n)"
98+
]
99+
},
100+
{
101+
"cell_type": "markdown",
102+
"metadata": {},
103+
"source": [
104+
"The pseudo-color image is numpy array with values between 0 and 1 (RGB)\nthat can be further processed or saved as needed.\n\n"
105+
]
106+
},
107+
{
108+
"cell_type": "markdown",
109+
"metadata": {},
110+
"source": [
111+
"## Elliptical cursors\n\nUse elliptical cursors to mask better-defined regions of interest in the\nphasor space. Elliptical cursors allow independent control of the radii,\nwhich can better match elongated clusters in phasor space:\n\n"
112+
]
113+
},
114+
{
115+
"cell_type": "code",
116+
"execution_count": null,
117+
"metadata": {
118+
"collapsed": false
119+
},
120+
"outputs": [],
121+
"source": [
122+
"radius = [0.1, 0.06] # major axis\nradius_minor = [0.3, 0.25] # minor axis\n\nelliptic_mask = mask_from_elliptic_cursor(\n real,\n imag,\n cursor_real,\n cursor_imag,\n radius=radius,\n radius_minor=radius_minor,\n)"
123+
]
124+
},
125+
{
126+
"cell_type": "markdown",
127+
"metadata": {},
128+
"source": [
129+
"Show the elliptical cursors in a phasor plot:\n\n"
130+
]
131+
},
132+
{
133+
"cell_type": "code",
134+
"execution_count": null,
135+
"metadata": {
136+
"collapsed": false
137+
},
138+
"outputs": [],
139+
"source": [
140+
"plot = PhasorPlot(allquadrants=True, title='Elliptical cursors')\nplot.hist2d(real, imag, cmap='Greys')\nplot.cursor(\n cursor_real,\n cursor_imag,\n radius=radius,\n radius_minor=radius_minor,\n color=CATEGORICAL[:2],\n label=['cursor 0', 'cursor 1'],\n)\nplot.show()"
141+
]
142+
},
143+
{
144+
"cell_type": "markdown",
145+
"metadata": {},
146+
"source": [
147+
"The mean intensity image can be used as a base layer to overlay\nthe masks from the elliptical cursors:\n\n"
148+
]
149+
},
150+
{
151+
"cell_type": "code",
152+
"execution_count": null,
153+
"metadata": {
154+
"collapsed": false
155+
},
156+
"outputs": [],
157+
"source": [
158+
"pseudo_color_image = pseudo_color(*elliptic_mask, intensity=mean)\n\nplot_image(\n pseudo_color_image,\n title='Pseudo-color image from elliptical cursors and intensity',\n)"
159+
]
160+
},
161+
{
162+
"cell_type": "markdown",
163+
"metadata": {},
164+
"source": [
165+
"## Polar cursors\n\nUse polar cursors to select regions of interest in the phasor space based\non phase and modulation ranges:\n\n"
166+
]
167+
},
168+
{
169+
"cell_type": "code",
170+
"execution_count": null,
171+
"metadata": {
172+
"collapsed": false
173+
},
174+
"outputs": [],
175+
"source": [
176+
"phase_min = [-2.27, -1.22]\nphase_max = [-1.57, -0.70]\nmodulation_min = [0.7, 0.8]\nmodulation_max = [0.9, 1.0]\n\npolar_mask = mask_from_polar_cursor(\n real, imag, phase_min, phase_max, modulation_min, modulation_max\n)"
177+
]
178+
},
179+
{
180+
"cell_type": "markdown",
181+
"metadata": {},
182+
"source": [
183+
"Show the polar cursors in a phasor plot:\n\n"
184+
]
185+
},
186+
{
187+
"cell_type": "code",
188+
"execution_count": null,
189+
"metadata": {
190+
"collapsed": false
191+
},
192+
"outputs": [],
193+
"source": [
194+
"plot = PhasorPlot(allquadrants=True, title='Polar cursors')\nplot.hist2d(real, imag, cmap='Greys')\nplot.polar_cursor(\n phase=phase_min,\n phase_limit=phase_max,\n modulation=modulation_min,\n modulation_limit=modulation_max,\n color=CATEGORICAL[2:4], # use different colors\n label=['cursor 0', 'cursor 1'],\n)\nplot.show()"
195+
]
196+
},
197+
{
198+
"cell_type": "markdown",
199+
"metadata": {},
200+
"source": [
201+
"The thresholded mean intensity image can be used as a base layer to\noverlay the masks from the polar cursors. Values below the threshold are\ntransparent (white):\n\n"
202+
]
203+
},
204+
{
205+
"cell_type": "code",
206+
"execution_count": null,
207+
"metadata": {
208+
"collapsed": false
209+
},
210+
"outputs": [],
211+
"source": [
212+
"pseudo_color_image = pseudo_color(\n *polar_mask, intensity=mean_thresholded, colors=CATEGORICAL[2:]\n)\n\nplot_image(\n pseudo_color_image,\n title='Pseudo-color image from\\npolar cursors and thresholded intensity',\n)"
213+
]
214+
}
215+
],
216+
"metadata": {
217+
"kernelspec": {
218+
"display_name": "Python 3",
219+
"language": "python",
220+
"name": "python3"
221+
},
222+
"language_info": {
223+
"codemirror_mode": {
224+
"name": "ipython",
225+
"version": 3
226+
},
227+
"file_extension": ".py",
228+
"mimetype": "text/x-python",
229+
"name": "python",
230+
"nbconvert_exporter": "python",
231+
"pygments_lexer": "ipython3",
232+
"version": "3.14.2"
233+
}
234+
},
235+
"nbformat": 4,
236+
"nbformat_minor": 0
237+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"\n# Benchmark phasor_from_signal\n\nBenchmark the ``phasor_from_signal`` function.\n\nThe :py:func:`phasorpy.phasor.phasor_from_signal` function used to calculate\nphasor coordinates from time-resolved or spectral signals can operate in\ntwo modes:\n\n- using an internal Cython function optimized for calculating a small number\n of harmonics, optionally using multiple threads.\n\n- using a real forward Fast Fourier Transform (FFT), ``numpy.fft.rfft`` or\n a drop-in replacement function like ``scipy.fft.rfft``\n or ``mkl_fft.interfaces.numpy_fft.rfft``.\n\nThis tutorial compares the performance of the two modes.\n\nImport required modules and functions:\n"
8+
]
9+
},
10+
{
11+
"cell_type": "code",
12+
"execution_count": null,
13+
"metadata": {
14+
"collapsed": false
15+
},
16+
"outputs": [],
17+
"source": [
18+
"from timeit import timeit\n\nimport numpy\nfrom numpy.fft import rfft as numpy_fft # noqa: F401\n\nfrom phasorpy.phasor import phasor_from_signal # noqa: F401\nfrom phasorpy.utils import number_threads\n\ntry:\n from scipy.fft import rfft as scipy_fft\nexcept ImportError:\n scipy_fft = None\n\ntry:\n from mkl_fft.interfaces.numpy_fft import rfft as mkl_fft\nexcept ImportError:\n mkl_fft = None\n\nrng = numpy.random.default_rng(42) # initialize random number generator"
19+
]
20+
},
21+
{
22+
"cell_type": "markdown",
23+
"metadata": {},
24+
"source": [
25+
"## Run benchmark\n\nCreate a random signal with a size and dtype similar to real-world data:\n\n"
26+
]
27+
},
28+
{
29+
"cell_type": "code",
30+
"execution_count": null,
31+
"metadata": {
32+
"collapsed": false
33+
},
34+
"outputs": [],
35+
"source": [
36+
"signal = rng.random((384, 384, 384))\nsignal += 1.1\nsignal *= 3723 # ~12 bit\nsignal = signal.astype(numpy.uint16) # 108 MB\nsignal[signal < 0.05] = 0.0 # 5% no signal"
37+
]
38+
},
39+
{
40+
"cell_type": "markdown",
41+
"metadata": {},
42+
"source": [
43+
"Print execution times depending on FFT function, axis, number of harmonics,\nand number of threads:\n\n"
44+
]
45+
},
46+
{
47+
"cell_type": "code",
48+
"execution_count": null,
49+
"metadata": {
50+
"collapsed": false
51+
},
52+
"outputs": [],
53+
"source": [
54+
"statement = \"\"\"\nphasor_from_signal(signal, axis=axis, harmonic=harmonic, **kwargs)\n\"\"\"\nnumber = 1 # how many times to execute statement\nref = None # reference duration\n\n\ndef print_(descr, t):\n print(f' {descr:20s}{t / number:>6.3f}s {t / ref:>6.2f}')\n\n\nfor harmonic in ([1], [1, 2, 3, 4, 5, 6, 7, 8]):\n print(f'harmonics {len(harmonic)}')\n for axis in (-1, 0, 2):\n print(f' axis {axis}')\n kwargs = {'use_fft': False, 'num_threads': 1}\n t = timeit(statement, number=number, globals=globals())\n if ref is None:\n ref = t\n print_('not_fft', t)\n\n num_threads = number_threads(0, 6)\n if num_threads > 1:\n kwargs = {'use_fft': False, 'num_threads': num_threads}\n t = timeit(statement, number=number, globals=globals())\n print_(f'not_fft ({num_threads} threads)', t)\n\n for fft_name in ('numpy_fft', 'scipy_fft', 'mkl_fft'):\n fft_func = globals()[fft_name]\n if fft_func is None:\n continue\n kwargs = {'use_fft': True, 'rfft': fft_func}\n t = timeit(statement, number=number, globals=globals())\n print_(f'{fft_name}', t)"
55+
]
56+
},
57+
{
58+
"cell_type": "markdown",
59+
"metadata": {},
60+
"source": [
61+
"For reference, the results on a Core i7-14700K CPU, Windows 11,\nPython 3.14.0, numpy 2.3.5, scipy 1.16.3, mkl_fft 2.1.1::\n\n harmonics 1\n axis -1\n not_fft 0.034s 1.00\n not_fft (6 threads) 0.006s 0.17\n numpy_fft 0.274s 8.03\n scipy_fft 0.240s 7.04\n mkl_fft 0.141s 4.14\n axis 0\n not_fft 0.162s 4.75\n not_fft (6 threads) 0.038s 1.13\n numpy_fft 0.697s 20.44\n scipy_fft 0.496s 14.54\n mkl_fft 0.167s 4.90\n axis 2\n not_fft 0.038s 1.12\n not_fft (6 threads) 0.006s 0.16\n numpy_fft 0.272s 7.99\n scipy_fft 0.240s 7.04\n mkl_fft 0.130s 3.83\n harmonics 8\n axis -1\n not_fft 0.287s 8.43\n not_fft (6 threads) 0.040s 1.17\n numpy_fft 0.288s 8.45\n scipy_fft 0.253s 7.43\n mkl_fft 0.161s 4.74\n axis 0\n not_fft 1.161s 34.04\n not_fft (6 threads) 0.425s 12.47\n numpy_fft 0.715s 20.97\n scipy_fft 0.541s 15.88\n mkl_fft 0.183s 5.38\n axis 2\n not_fft 0.282s 8.27\n not_fft (6 threads) 0.038s 1.13\n numpy_fft 0.288s 8.46\n scipy_fft 0.255s 7.49\n mkl_fft 0.155s 4.56\n\n"
62+
]
63+
},
64+
{
65+
"cell_type": "markdown",
66+
"metadata": {},
67+
"source": [
68+
"## Results\n\n- Using the Cython implementation is significantly faster than using the\n ``numpy.fft``-based implementation for single harmonics.\n- Using multiple threads can significantly speed up the Cython mode.\n- The FFT functions from ``scipy`` and ``mkl_fft`` outperform the\n ``numpy.fft`` function. Specifically, ``mkl_fft`` is very performant.\n- Using FFT becomes more competitive when calculating a larger number of\n harmonics.\n- Computing over the last axis is significantly faster compared to the first\n axis. That is because the samples in the last dimension are contiguous in\n memory.\n\nNote that these results were obtained on a single dataset of random numbers.\n\n"
69+
]
70+
},
71+
{
72+
"cell_type": "markdown",
73+
"metadata": {},
74+
"source": [
75+
"## Conclusions\n\nUsing the Cython implementation is a reasonable default when calculating\na few harmonics. Using FFT is a better choice when computing a large number\nof harmonics, especially with an optimized FFT function.\n\n"
76+
]
77+
}
78+
],
79+
"metadata": {
80+
"kernelspec": {
81+
"display_name": "Python 3",
82+
"language": "python",
83+
"name": "python3"
84+
},
85+
"language_info": {
86+
"codemirror_mode": {
87+
"name": "ipython",
88+
"version": 3
89+
},
90+
"file_extension": ".py",
91+
"mimetype": "text/x-python",
92+
"name": "python",
93+
"nbconvert_exporter": "python",
94+
"pygments_lexer": "ipython3",
95+
"version": "3.14.2"
96+
}
97+
},
98+
"nbformat": 4,
99+
"nbformat_minor": 0
100+
}

0 commit comments

Comments
 (0)