flimkit 0.12.0__py3-none-any.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.
- flimkit/FLIM/__init__.py +0 -0
- flimkit/FLIM/assemble.py +254 -0
- flimkit/FLIM/batch.py +681 -0
- flimkit/FLIM/bg_tools.py +51 -0
- flimkit/FLIM/fit_tools.py +244 -0
- flimkit/FLIM/fitters.py +1471 -0
- flimkit/FLIM/irf_tools.py +617 -0
- flimkit/FLIM/models.py +391 -0
- flimkit/GPU/__init__.py +85 -0
- flimkit/GPU/_base.py +391 -0
- flimkit/GPU/cuda.py +10 -0
- flimkit/GPU/mlx_backend.py +381 -0
- flimkit/GPU/mps.py +10 -0
- flimkit/GPU/rocm.py +10 -0
- flimkit/GPU/torch_backend.py +385 -0
- flimkit/UI/app_state.py +10 -0
- flimkit/UI/controller.py +139 -0
- flimkit/UI/expert_settings.py +248 -0
- flimkit/UI/fit_help.py +206 -0
- flimkit/UI/fov_preview.py +1085 -0
- flimkit/UI/gui.py +3919 -0
- flimkit/UI/icon.icns +0 -0
- flimkit/UI/icon.ico +0 -0
- flimkit/UI/icon.png +0 -0
- flimkit/UI/irf_widget.py +103 -0
- flimkit/UI/mode_controller.py +118 -0
- flimkit/UI/modes/__init__.py +0 -0
- flimkit/UI/modes/base.py +3 -0
- flimkit/UI/modes/batch_mode.py +312 -0
- flimkit/UI/modes/fov_mode.py +164 -0
- flimkit/UI/modes/irf_mode.py +80 -0
- flimkit/UI/modes/phasor_mode.py +131 -0
- flimkit/UI/modes/stitch_mode.py +254 -0
- flimkit/UI/phasor_panel.py +1087 -0
- flimkit/UI/progress_window.py +113 -0
- flimkit/UI/project_panel.py +262 -0
- flimkit/UI/results_panel.py +332 -0
- flimkit/UI/roi_tools.py +794 -0
- flimkit/UI/utils.py +217 -0
- flimkit/__init__.py +0 -0
- flimkit/_version.py +41 -0
- flimkit/cli.py +120 -0
- flimkit/configs.py +148 -0
- flimkit/dialogs.py +46 -0
- flimkit/formats/BH/__init__.py +0 -0
- flimkit/formats/BH/reader.py +296 -0
- flimkit/formats/BH/writer.py +86 -0
- flimkit/formats/ISS/__init__.py +0 -0
- flimkit/formats/ISS/fdflim.py +86 -0
- flimkit/formats/ISS/image.py +114 -0
- flimkit/formats/ISS/reader.py +223 -0
- flimkit/formats/PS/__init__.py +0 -0
- flimkit/formats/PS/reader.py +202 -0
- flimkit/formats/PTU/__init__.py +0 -0
- flimkit/formats/PTU/decode.py +27 -0
- flimkit/formats/PTU/phu.py +85 -0
- flimkit/formats/PTU/reader.py +235 -0
- flimkit/formats/PTU/series.py +258 -0
- flimkit/formats/PTU/stitch.py +1182 -0
- flimkit/formats/PTU/tools.py +94 -0
- flimkit/formats/__init__.py +2 -0
- flimkit/formats/flim_file.py +232 -0
- flimkit/formats/phasor.py +132 -0
- flimkit/formats/signal.py +170 -0
- flimkit/image/tools.py +124 -0
- flimkit/interactive.py +1857 -0
- flimkit/mpl_backend.py +22 -0
- flimkit/phasor/__init__.py +40 -0
- flimkit/phasor/filters.py +127 -0
- flimkit/phasor/fret.py +654 -0
- flimkit/phasor/interactive.py +556 -0
- flimkit/phasor/peaks.py +186 -0
- flimkit/phasor/signal.py +90 -0
- flimkit/phasor_launcher.py +314 -0
- flimkit/plugins/__init__.py +137 -0
- flimkit/plugins/bindings.py +116 -0
- flimkit/plugins/builtin/__init__.py +3 -0
- flimkit/plugins/builtin/core_tools.py +28 -0
- flimkit/plugins/loader.py +371 -0
- flimkit/plugins/registry.py +406 -0
- flimkit/project.py +197 -0
- flimkit/synth.py +145 -0
- flimkit/utils/__init__.py +0 -0
- flimkit/utils/batch_fit.py +301 -0
- flimkit/utils/config_manager.py +119 -0
- flimkit/utils/config_snapshot.py +30 -0
- flimkit/utils/crash_handler.py +183 -0
- flimkit/utils/display.py +197 -0
- flimkit/utils/enhanced_outputs.py +345 -0
- flimkit/utils/fancy.py +103 -0
- flimkit/utils/lifetime_image.py +243 -0
- flimkit/utils/misc.py +111 -0
- flimkit/utils/plotting.py +190 -0
- flimkit/utils/roi.py +370 -0
- flimkit/utils/session.py +51 -0
- flimkit/utils/update_check.py +198 -0
- flimkit/utils/xlsx_tools.py +97 -0
- flimkit/utils/xml_utils.py +219 -0
- flimkit-0.12.0.dist-info/METADATA +356 -0
- flimkit-0.12.0.dist-info/RECORD +104 -0
- flimkit-0.12.0.dist-info/WHEEL +5 -0
- flimkit-0.12.0.dist-info/entry_points.txt +2 -0
- flimkit-0.12.0.dist-info/licenses/LICENSE.md +11 -0
- flimkit-0.12.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
from matplotlib.patches import Ellipse
|
|
4
|
+
|
|
5
|
+
from phasorpy.plot import PhasorPlot
|
|
6
|
+
from phasorpy.cursor import mask_from_elliptic_cursor, pseudo_color
|
|
7
|
+
from phasorpy.lifetime import (
|
|
8
|
+
phasor_to_apparent_lifetime,
|
|
9
|
+
phasor_semicircle_intersect,
|
|
10
|
+
)
|
|
11
|
+
from phasorpy.component import phasor_component_fraction
|
|
12
|
+
|
|
13
|
+
_COLORS = ['#d62728', '#1f77b4', '#2ca02c', '#ff7f0e', '#9467bd', '#8c564b']
|
|
14
|
+
|
|
15
|
+
def _in_notebook():
|
|
16
|
+
try:
|
|
17
|
+
from IPython import get_ipython
|
|
18
|
+
shell = get_ipython()
|
|
19
|
+
if shell is None:
|
|
20
|
+
return False
|
|
21
|
+
return shell.__class__.__name__ == 'ZMQInteractiveShell'
|
|
22
|
+
except Exception:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
def _ellipse_angle_rad(cg, cs, mode):
|
|
26
|
+
if mode == 'semicircle':
|
|
27
|
+
return np.arctan2(cs, cg - 0.5) + np.pi / 2.0
|
|
28
|
+
return np.arctan2(cs, cg)
|
|
29
|
+
|
|
30
|
+
def phasor_cursor_tool(
|
|
31
|
+
real_cal,
|
|
32
|
+
imag_cal,
|
|
33
|
+
mean,
|
|
34
|
+
frequency,
|
|
35
|
+
*,
|
|
36
|
+
display_image=None,
|
|
37
|
+
min_photons=0.01,
|
|
38
|
+
max_cursors=6,
|
|
39
|
+
figsize=(8, 5),
|
|
40
|
+
initial_cursors=None,
|
|
41
|
+
initial_params=None,
|
|
42
|
+
on_save=None,
|
|
43
|
+
):
|
|
44
|
+
notebook = _in_notebook()
|
|
45
|
+
|
|
46
|
+
rc = np.asarray(real_cal).squeeze().astype(float)
|
|
47
|
+
ic = np.asarray(imag_cal).squeeze().astype(float)
|
|
48
|
+
mn = np.asarray(mean).squeeze().astype(float)
|
|
49
|
+
|
|
50
|
+
disp = (np.asarray(display_image).squeeze().astype(float)
|
|
51
|
+
if display_image is not None else mn)
|
|
52
|
+
valid = (mn >= min_photons) & ~np.isnan(rc)
|
|
53
|
+
g_all = rc[valid]
|
|
54
|
+
s_all = ic[valid]
|
|
55
|
+
|
|
56
|
+
state = {
|
|
57
|
+
'cursors': [],
|
|
58
|
+
'masks': None,
|
|
59
|
+
'fig': None,
|
|
60
|
+
'ax': None,
|
|
61
|
+
}
|
|
62
|
+
cursor_artists = []
|
|
63
|
+
|
|
64
|
+
params = dict(radius=0.05, radius_minor=0.03, angle_mode='semicircle')
|
|
65
|
+
if initial_params:
|
|
66
|
+
params.update({k: v for k, v in initial_params.items()
|
|
67
|
+
if k in params})
|
|
68
|
+
|
|
69
|
+
if notebook:
|
|
70
|
+
import ipywidgets as widgets
|
|
71
|
+
from IPython.display import display, clear_output
|
|
72
|
+
results_out = widgets.Output()
|
|
73
|
+
|
|
74
|
+
def _begin_output():
|
|
75
|
+
results_out.__enter__()
|
|
76
|
+
clear_output(wait=True)
|
|
77
|
+
|
|
78
|
+
def _end_output():
|
|
79
|
+
results_out.__exit__(None, None, None)
|
|
80
|
+
else:
|
|
81
|
+
def _begin_output():
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
def _end_output():
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
def _redraw():
|
|
88
|
+
ax = state['ax']
|
|
89
|
+
for art in cursor_artists:
|
|
90
|
+
try:
|
|
91
|
+
art.remove()
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
cursor_artists.clear()
|
|
95
|
+
|
|
96
|
+
radius = params['radius']
|
|
97
|
+
r_minor = params['radius_minor']
|
|
98
|
+
mode = params['angle_mode']
|
|
99
|
+
cursors = state['cursors']
|
|
100
|
+
|
|
101
|
+
for i, cur in enumerate(cursors):
|
|
102
|
+
cg, cs, col = cur['center_g'], cur['center_s'], cur['color']
|
|
103
|
+
ang_deg = np.degrees(_ellipse_angle_rad(cg, cs, mode))
|
|
104
|
+
|
|
105
|
+
ell = Ellipse(
|
|
106
|
+
(cg, cs), 2 * radius, 2 * r_minor,
|
|
107
|
+
angle=ang_deg, facecolor=col, alpha=0.20,
|
|
108
|
+
edgecolor=col, linewidth=2, linestyle='--', zorder=8)
|
|
109
|
+
ax.add_patch(ell)
|
|
110
|
+
cursor_artists.append(ell)
|
|
111
|
+
|
|
112
|
+
(dot,) = ax.plot(cg, cs, 'o', color=col, ms=8, zorder=10)
|
|
113
|
+
cursor_artists.append(dot)
|
|
114
|
+
txt = ax.text(cg + 0.015, cs + 0.015, f'C{i+1}',
|
|
115
|
+
color=col, fontsize=10, fontweight='bold',
|
|
116
|
+
zorder=11)
|
|
117
|
+
cursor_artists.append(txt)
|
|
118
|
+
|
|
119
|
+
if len(cursors) >= 2:
|
|
120
|
+
g0, s0 = cursors[0]['center_g'], cursors[0]['center_s']
|
|
121
|
+
g1, s1 = cursors[1]['center_g'], cursors[1]['center_s']
|
|
122
|
+
gi0, si0, gi1, si1 = phasor_semicircle_intersect(
|
|
123
|
+
g0, s0, g1, s1)
|
|
124
|
+
if not np.isnan(gi0):
|
|
125
|
+
(ln,) = ax.plot(
|
|
126
|
+
[float(gi0), float(gi1)],
|
|
127
|
+
[float(si0), float(si1)],
|
|
128
|
+
'r-', lw=1.5, alpha=0.6, zorder=7)
|
|
129
|
+
cursor_artists.append(ln)
|
|
130
|
+
(st1,) = ax.plot(float(gi0), float(si0),
|
|
131
|
+
'g*', ms=12, zorder=12)
|
|
132
|
+
(st2,) = ax.plot(float(gi1), float(si1),
|
|
133
|
+
'm*', ms=12, zorder=12)
|
|
134
|
+
cursor_artists.extend([st1, st2])
|
|
135
|
+
|
|
136
|
+
ax.figure.canvas.draw_idle()
|
|
137
|
+
|
|
138
|
+
def _analyse():
|
|
139
|
+
cursors = state['cursors']
|
|
140
|
+
if not cursors:
|
|
141
|
+
state['masks'] = None
|
|
142
|
+
_begin_output()
|
|
143
|
+
print('Click on the phasor plot to place elliptical cursors.')
|
|
144
|
+
_end_output()
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
radius = params['radius']
|
|
148
|
+
r_minor = params['radius_minor']
|
|
149
|
+
mode = params['angle_mode']
|
|
150
|
+
n_cursors = len(cursors)
|
|
151
|
+
|
|
152
|
+
disp_arr = np.asarray(disp)
|
|
153
|
+
if disp_arr.ndim != 2 or min(disp_arr.shape) < 2:
|
|
154
|
+
|
|
155
|
+
_begin_output()
|
|
156
|
+
g_pt = float(np.ravel(rc)[0])
|
|
157
|
+
s_pt = float(np.ravel(ic)[0])
|
|
158
|
+
tau_phi_pt = float(phasor_to_apparent_lifetime(g_pt, s_pt, frequency)[0])
|
|
159
|
+
fig2, ax_pt = plt.subplots(1, 1, figsize=(5, 4))
|
|
160
|
+
ax_pt.text(0.5, 0.5, 'No image available\n(point measurement)',
|
|
161
|
+
ha='center', va='center', fontsize=11)
|
|
162
|
+
ax_pt.set_xticks([])
|
|
163
|
+
ax_pt.set_yticks([])
|
|
164
|
+
ax_pt.set_title(f'τ_φ = {tau_phi_pt:.2f} ns (G={g_pt:.3f}, S={s_pt:.3f})')
|
|
165
|
+
fig2.tight_layout()
|
|
166
|
+
plt.show()
|
|
167
|
+
print(f'Point measurement: τ_φ = {tau_phi_pt:.2f} ns '
|
|
168
|
+
f'(G={g_pt:.3f}, S={s_pt:.3f})')
|
|
169
|
+
_end_output()
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
centers_g = np.array([c['center_g'] for c in cursors])
|
|
173
|
+
centers_s = np.array([c['center_s'] for c in cursors])
|
|
174
|
+
|
|
175
|
+
masks = mask_from_elliptic_cursor(
|
|
176
|
+
rc, ic, centers_g, centers_s,
|
|
177
|
+
radius=radius, radius_minor=r_minor, angle=mode,
|
|
178
|
+
)
|
|
179
|
+
if masks.ndim == rc.ndim:
|
|
180
|
+
masks = masks[np.newaxis]
|
|
181
|
+
masks = masks & valid[np.newaxis]
|
|
182
|
+
state['masks'] = masks
|
|
183
|
+
|
|
184
|
+
_begin_output()
|
|
185
|
+
|
|
186
|
+
has_decomp = n_cursors >= 2
|
|
187
|
+
n_cols = 1 + n_cursors + (1 if has_decomp else 0)
|
|
188
|
+
fig2, axes2 = plt.subplots(
|
|
189
|
+
1, n_cols, figsize=(4.5 * n_cols, 4.5))
|
|
190
|
+
if n_cols == 1:
|
|
191
|
+
axes2 = [axes2]
|
|
192
|
+
|
|
193
|
+
ax_pc = axes2[0]
|
|
194
|
+
mask_list = [masks[i] for i in range(n_cursors)]
|
|
195
|
+
colors_rgb = np.array([
|
|
196
|
+
[int(c['color'][1:3], 16) / 255,
|
|
197
|
+
int(c['color'][3:5], 16) / 255,
|
|
198
|
+
int(c['color'][5:7], 16) / 255]
|
|
199
|
+
for c in cursors
|
|
200
|
+
])
|
|
201
|
+
pc_img = pseudo_color(
|
|
202
|
+
*mask_list, intensity=disp, colors=colors_rgb)
|
|
203
|
+
ax_pc.imshow(pc_img, origin='upper')
|
|
204
|
+
ax_pc.set_title('Pseudo-color overlay')
|
|
205
|
+
for i, c in enumerate(cursors):
|
|
206
|
+
ax_pc.text(
|
|
207
|
+
5, 15 + i * 15,
|
|
208
|
+
f'C{i+1}: {int(masks[i].sum())} px',
|
|
209
|
+
color=c['color'], fontsize=9, fontweight='bold')
|
|
210
|
+
|
|
211
|
+
for ci in range(n_cursors):
|
|
212
|
+
ax_cur = axes2[1 + ci]
|
|
213
|
+
mask_i = masks[ci]
|
|
214
|
+
n_px = int(mask_i.sum())
|
|
215
|
+
if n_px == 0:
|
|
216
|
+
ax_cur.set_title(f'C{ci+1}: no pixels')
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
g_sel = rc[mask_i]
|
|
220
|
+
s_sel = ic[mask_i]
|
|
221
|
+
tau_phi, tau_mod = phasor_to_apparent_lifetime(
|
|
222
|
+
g_sel, s_sel, frequency)
|
|
223
|
+
|
|
224
|
+
valid_mod = tau_mod[np.isfinite(tau_mod)]
|
|
225
|
+
|
|
226
|
+
fmap = np.full_like(rc, np.nan)
|
|
227
|
+
fmap[mask_i] = tau_phi
|
|
228
|
+
vlo = np.nanpercentile(tau_phi, 2)
|
|
229
|
+
vhi = np.nanpercentile(tau_phi, 98)
|
|
230
|
+
im = ax_cur.imshow(
|
|
231
|
+
fmap, cmap='turbo', vmin=vlo, vmax=vhi,
|
|
232
|
+
origin='upper')
|
|
233
|
+
plt.colorbar(im, ax=ax_cur, label='τ_φ (ns)')
|
|
234
|
+
|
|
235
|
+
med = np.nanmedian(tau_phi)
|
|
236
|
+
med_mod = float(np.median(valid_mod)) if valid_mod.size > 0 else float('nan')
|
|
237
|
+
mod_str = f'{med_mod:.2f}' if np.isfinite(med_mod) else 'n/a'
|
|
238
|
+
ax_cur.set_title(
|
|
239
|
+
f'C{ci+1}: τ_φ med={med:.2f}, τ_m med={mod_str} ns (n={n_px})')
|
|
240
|
+
print(
|
|
241
|
+
f"C{ci+1}: {n_px} px "
|
|
242
|
+
f"τ_φ = {np.nanmin(tau_phi):.2f}"
|
|
243
|
+
f"-{np.nanmax(tau_phi):.2f} ns "
|
|
244
|
+
f"(med {med:.2f}) │ "
|
|
245
|
+
f"τ_m med = {mod_str} ns")
|
|
246
|
+
|
|
247
|
+
if has_decomp:
|
|
248
|
+
ax_dec = axes2[-1]
|
|
249
|
+
g0, s0 = centers_g[0], centers_s[0]
|
|
250
|
+
g1, s1 = centers_g[1], centers_s[1]
|
|
251
|
+
gi0, si0, gi1, si1 = phasor_semicircle_intersect(
|
|
252
|
+
g0, s0, g1, s1)
|
|
253
|
+
|
|
254
|
+
if not (np.isnan(gi0) or np.isnan(gi1)):
|
|
255
|
+
tau1 = float(phasor_to_apparent_lifetime(
|
|
256
|
+
gi0, si0, frequency)[0])
|
|
257
|
+
tau2 = float(phasor_to_apparent_lifetime(
|
|
258
|
+
gi1, si1, frequency)[0])
|
|
259
|
+
|
|
260
|
+
combined = np.any(masks, axis=0)
|
|
261
|
+
g_comb = rc[combined]
|
|
262
|
+
s_comb = ic[combined]
|
|
263
|
+
|
|
264
|
+
frac = phasor_component_fraction(
|
|
265
|
+
g_comb, s_comb,
|
|
266
|
+
[float(gi0), float(gi1)],
|
|
267
|
+
[float(si0), float(si1)],
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
theta = np.linspace(0, np.pi, 200)
|
|
271
|
+
ax_dec.plot(
|
|
272
|
+
0.5 + 0.5 * np.cos(theta),
|
|
273
|
+
0.5 * np.sin(theta),
|
|
274
|
+
'k-', lw=1, alpha=0.6)
|
|
275
|
+
sc = ax_dec.scatter(
|
|
276
|
+
g_comb, s_comb, c=frac, s=2,
|
|
277
|
+
cmap='RdYlBu_r', vmin=0, vmax=1,
|
|
278
|
+
alpha=0.5, zorder=5)
|
|
279
|
+
plt.colorbar(sc, ax=ax_dec, label='fraction C1')
|
|
280
|
+
ax_dec.plot(
|
|
281
|
+
[float(gi0), float(gi1)],
|
|
282
|
+
[float(si0), float(si1)],
|
|
283
|
+
'r-', lw=2, zorder=10)
|
|
284
|
+
ax_dec.plot(
|
|
285
|
+
float(gi0), float(si0), 'g*', ms=14,
|
|
286
|
+
zorder=15, label=f'τ₁={tau1:.2f} ns')
|
|
287
|
+
ax_dec.plot(
|
|
288
|
+
float(gi1), float(si1), 'm*', ms=14,
|
|
289
|
+
zorder=15, label=f'τ₂={tau2:.2f} ns')
|
|
290
|
+
ax_dec.set_xlabel('G')
|
|
291
|
+
ax_dec.set_ylabel('S')
|
|
292
|
+
ax_dec.set_xlim(0, 1.05)
|
|
293
|
+
ax_dec.set_ylim(-0.02, 0.55)
|
|
294
|
+
ax_dec.set_aspect('equal')
|
|
295
|
+
ax_dec.legend(fontsize=9, loc='upper left')
|
|
296
|
+
ax_dec.set_title('Two-component (C1↔C2)')
|
|
297
|
+
|
|
298
|
+
print(
|
|
299
|
+
f"\n═══ Two-component decomposition (C1-C2) ═══")
|
|
300
|
+
print(
|
|
301
|
+
f" τ₁ = {tau1:.3f} ns "
|
|
302
|
+
f"(G={float(gi0):.4f}, S={float(si0):.4f})")
|
|
303
|
+
print(
|
|
304
|
+
f" τ₂ = {tau2:.3f} ns "
|
|
305
|
+
f"(G={float(gi1):.4f}, S={float(si1):.4f})")
|
|
306
|
+
print(
|
|
307
|
+
f" Mean fraction(τ₁) = "
|
|
308
|
+
f"{float(np.mean(frac)):.3f}")
|
|
309
|
+
else:
|
|
310
|
+
ax_dec.text(
|
|
311
|
+
0.5, 0.5,
|
|
312
|
+
'C1↔C2 line does not\nintersect semicircle',
|
|
313
|
+
ha='center', va='center',
|
|
314
|
+
transform=ax_dec.transAxes)
|
|
315
|
+
ax_dec.set_title('Two-component (C1↔C2)')
|
|
316
|
+
|
|
317
|
+
fig2.tight_layout()
|
|
318
|
+
plt.show()
|
|
319
|
+
_end_output()
|
|
320
|
+
|
|
321
|
+
def _on_click(event):
|
|
322
|
+
if event.inaxes != state['ax'] or event.button != 1:
|
|
323
|
+
return
|
|
324
|
+
if event.xdata is None:
|
|
325
|
+
return
|
|
326
|
+
cursors = state['cursors']
|
|
327
|
+
if len(cursors) >= max_cursors:
|
|
328
|
+
return
|
|
329
|
+
idx = len(cursors) % len(_COLORS)
|
|
330
|
+
cursors.append(dict(
|
|
331
|
+
center_g=event.xdata,
|
|
332
|
+
center_s=event.ydata,
|
|
333
|
+
color=_COLORS[idx]))
|
|
334
|
+
_redraw()
|
|
335
|
+
_analyse()
|
|
336
|
+
|
|
337
|
+
def _on_clear(_ignored=None):
|
|
338
|
+
state['cursors'].clear()
|
|
339
|
+
state['masks'] = None
|
|
340
|
+
_redraw()
|
|
341
|
+
_begin_output()
|
|
342
|
+
print('Cleared. Click on the phasor plot to place cursors.')
|
|
343
|
+
_end_output()
|
|
344
|
+
|
|
345
|
+
def _on_undo(_ignored=None):
|
|
346
|
+
cursors = state['cursors']
|
|
347
|
+
if cursors:
|
|
348
|
+
cursors.pop()
|
|
349
|
+
_redraw()
|
|
350
|
+
if cursors:
|
|
351
|
+
_analyse()
|
|
352
|
+
else:
|
|
353
|
+
state['masks'] = None
|
|
354
|
+
_begin_output()
|
|
355
|
+
print('All cursors removed. Click to place new ones.')
|
|
356
|
+
_end_output()
|
|
357
|
+
|
|
358
|
+
def _on_param_change(_ignored=None):
|
|
359
|
+
_redraw()
|
|
360
|
+
if state['cursors']:
|
|
361
|
+
_analyse()
|
|
362
|
+
|
|
363
|
+
if notebook:
|
|
364
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
365
|
+
else:
|
|
366
|
+
|
|
367
|
+
fig, ax = plt.subplots(figsize=(figsize[0], figsize[1] + 1.8))
|
|
368
|
+
fig.subplots_adjust(bottom=0.30)
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
fig.canvas.toolbar_visible = True
|
|
372
|
+
fig.canvas.header_visible = False
|
|
373
|
+
except Exception:
|
|
374
|
+
pass
|
|
375
|
+
|
|
376
|
+
pp = PhasorPlot(
|
|
377
|
+
frequency=frequency, ax=ax,
|
|
378
|
+
title=f'Click to place elliptical cursors (up to {max_cursors})')
|
|
379
|
+
pp.hist2d(g_all, s_all)
|
|
380
|
+
|
|
381
|
+
state['fig'] = fig
|
|
382
|
+
state['ax'] = ax
|
|
383
|
+
|
|
384
|
+
fig.canvas.mpl_connect('button_press_event', _on_click)
|
|
385
|
+
|
|
386
|
+
if initial_cursors:
|
|
387
|
+
for cur in initial_cursors:
|
|
388
|
+
state['cursors'].append(dict(
|
|
389
|
+
center_g=cur['center_g'],
|
|
390
|
+
center_s=cur['center_s'],
|
|
391
|
+
color=cur.get('color', _COLORS[
|
|
392
|
+
len(state['cursors']) % len(_COLORS)])))
|
|
393
|
+
_redraw()
|
|
394
|
+
_analyse()
|
|
395
|
+
|
|
396
|
+
def _on_save(_ignored=None):
|
|
397
|
+
if on_save is not None:
|
|
398
|
+
on_save(state, params)
|
|
399
|
+
|
|
400
|
+
def _on_export(_ignored=None):
|
|
401
|
+
from ..dialogs import ask_save_path
|
|
402
|
+
path = ask_save_path(
|
|
403
|
+
'Export phasor figure', 'phasor_plot.png', defaultextension='.png',
|
|
404
|
+
filetypes=[('PNG', '*.png'), ('PDF', '*.pdf'),
|
|
405
|
+
('SVG', '*.svg'), ('All files', '*')])
|
|
406
|
+
if path:
|
|
407
|
+
state['fig'].savefig(path, dpi=300, bbox_inches='tight')
|
|
408
|
+
print(f"Figure exported → {path}")
|
|
409
|
+
|
|
410
|
+
_peak_artists: list = []
|
|
411
|
+
|
|
412
|
+
def _on_peaks(_ignored=None):
|
|
413
|
+
from .peaks import find_phasor_peaks, print_peaks, _annotate_peaks
|
|
414
|
+
|
|
415
|
+
for art in _peak_artists:
|
|
416
|
+
try:
|
|
417
|
+
art.remove()
|
|
418
|
+
except Exception:
|
|
419
|
+
pass
|
|
420
|
+
_peak_artists.clear()
|
|
421
|
+
|
|
422
|
+
_begin_output()
|
|
423
|
+
peaks = find_phasor_peaks(rc, ic, mn, frequency,
|
|
424
|
+
min_photons=min_photons)
|
|
425
|
+
state['peaks'] = peaks
|
|
426
|
+
print_peaks(peaks)
|
|
427
|
+
arts = _annotate_peaks(
|
|
428
|
+
state['ax'], peaks['n_peaks'],
|
|
429
|
+
peaks['peak_g'], peaks['peak_s'], peaks['tau_phase'])
|
|
430
|
+
_peak_artists.extend(arts)
|
|
431
|
+
state['ax'].figure.canvas.draw_idle()
|
|
432
|
+
_end_output()
|
|
433
|
+
|
|
434
|
+
if notebook:
|
|
435
|
+
import ipywidgets as widgets
|
|
436
|
+
from IPython.display import display
|
|
437
|
+
|
|
438
|
+
w_radius = widgets.FloatSlider(
|
|
439
|
+
value=params['radius'], min=0.01, max=0.25, step=0.005,
|
|
440
|
+
description='Major radius:', continuous_update=False,
|
|
441
|
+
style={'description_width': 'initial'})
|
|
442
|
+
w_radius_minor = widgets.FloatSlider(
|
|
443
|
+
value=params['radius_minor'], min=0.005, max=0.15, step=0.005,
|
|
444
|
+
description='Minor radius:', continuous_update=False,
|
|
445
|
+
style={'description_width': 'initial'})
|
|
446
|
+
w_angle_mode = widgets.Dropdown(
|
|
447
|
+
options=['semicircle', 'phase'],
|
|
448
|
+
value=params['angle_mode'], description='Alignment:',
|
|
449
|
+
style={'description_width': 'initial'})
|
|
450
|
+
btn_clear = widgets.Button(description='Clear all',
|
|
451
|
+
button_style='warning')
|
|
452
|
+
btn_undo = widgets.Button(description='Undo last', button_style='')
|
|
453
|
+
btn_save = widgets.Button(description='💾 Save',
|
|
454
|
+
button_style='success') if on_save else None
|
|
455
|
+
btn_export = widgets.Button(description='Export',
|
|
456
|
+
button_style='info')
|
|
457
|
+
btn_peaks = widgets.Button(description='Peaks',
|
|
458
|
+
button_style='')
|
|
459
|
+
|
|
460
|
+
def _ipyw_radius(change):
|
|
461
|
+
params['radius'] = change['new']
|
|
462
|
+
_on_param_change()
|
|
463
|
+
|
|
464
|
+
def _ipyw_radius_minor(change):
|
|
465
|
+
params['radius_minor'] = change['new']
|
|
466
|
+
_on_param_change()
|
|
467
|
+
|
|
468
|
+
def _ipyw_angle(change):
|
|
469
|
+
params['angle_mode'] = change['new']
|
|
470
|
+
_on_param_change()
|
|
471
|
+
|
|
472
|
+
w_radius.observe(_ipyw_radius, names='value')
|
|
473
|
+
w_radius_minor.observe(_ipyw_radius_minor, names='value')
|
|
474
|
+
w_angle_mode.observe(_ipyw_angle, names='value')
|
|
475
|
+
btn_clear.on_click(_on_clear)
|
|
476
|
+
btn_undo.on_click(_on_undo)
|
|
477
|
+
btn_export.on_click(_on_export)
|
|
478
|
+
btn_peaks.on_click(_on_peaks)
|
|
479
|
+
if btn_save:
|
|
480
|
+
btn_save.on_click(_on_save)
|
|
481
|
+
|
|
482
|
+
btn_row = [btn_undo, btn_clear, btn_peaks, btn_export]
|
|
483
|
+
if btn_save:
|
|
484
|
+
btn_row.append(btn_save)
|
|
485
|
+
controls = widgets.HBox([
|
|
486
|
+
widgets.VBox([w_radius, w_radius_minor]),
|
|
487
|
+
widgets.VBox([w_angle_mode,
|
|
488
|
+
widgets.HBox(btn_row)]),
|
|
489
|
+
])
|
|
490
|
+
display(controls)
|
|
491
|
+
display(results_out)
|
|
492
|
+
|
|
493
|
+
else:
|
|
494
|
+
from matplotlib.widgets import Slider, Button, RadioButtons
|
|
495
|
+
|
|
496
|
+
ax_rad = fig.add_axes([0.15, 0.17, 0.55, 0.03])
|
|
497
|
+
ax_rmin = fig.add_axes([0.15, 0.12, 0.55, 0.03])
|
|
498
|
+
sl_radius = Slider(ax_rad, 'Major R', 0.01, 0.25,
|
|
499
|
+
valinit=params['radius'], valstep=0.005)
|
|
500
|
+
sl_radius_minor = Slider(ax_rmin, 'Minor R', 0.005, 0.15,
|
|
501
|
+
valinit=params['radius_minor'],
|
|
502
|
+
valstep=0.005)
|
|
503
|
+
|
|
504
|
+
ax_angle = fig.add_axes([0.15, 0.02, 0.15, 0.08])
|
|
505
|
+
rb_angle = RadioButtons(ax_angle, ['semicircle', 'phase'],
|
|
506
|
+
active=0)
|
|
507
|
+
|
|
508
|
+
ax_undo = fig.add_axes([0.35, 0.02, 0.10, 0.05])
|
|
509
|
+
ax_clr = fig.add_axes([0.46, 0.02, 0.10, 0.05])
|
|
510
|
+
ax_peaks = fig.add_axes([0.57, 0.02, 0.10, 0.05])
|
|
511
|
+
ax_exp = fig.add_axes([0.68, 0.02, 0.10, 0.05])
|
|
512
|
+
mpl_btn_undo = Button(ax_undo, 'Undo')
|
|
513
|
+
mpl_btn_clear = Button(ax_clr, 'Clear')
|
|
514
|
+
mpl_btn_peaks = Button(ax_peaks, 'Peaks')
|
|
515
|
+
mpl_btn_export = Button(ax_exp, 'Export')
|
|
516
|
+
if on_save:
|
|
517
|
+
ax_save = fig.add_axes([0.79, 0.02, 0.10, 0.05])
|
|
518
|
+
mpl_btn_save = Button(ax_save, 'Save')
|
|
519
|
+
else:
|
|
520
|
+
mpl_btn_save = None
|
|
521
|
+
|
|
522
|
+
def _mpl_radius(val):
|
|
523
|
+
params['radius'] = val
|
|
524
|
+
_on_param_change()
|
|
525
|
+
|
|
526
|
+
def _mpl_rminor(val):
|
|
527
|
+
params['radius_minor'] = val
|
|
528
|
+
_on_param_change()
|
|
529
|
+
|
|
530
|
+
def _mpl_angle(label):
|
|
531
|
+
params['angle_mode'] = label
|
|
532
|
+
_on_param_change()
|
|
533
|
+
|
|
534
|
+
sl_radius.on_changed(_mpl_radius)
|
|
535
|
+
sl_radius_minor.on_changed(_mpl_rminor)
|
|
536
|
+
rb_angle.on_clicked(_mpl_angle)
|
|
537
|
+
mpl_btn_undo.on_clicked(lambda _: _on_undo())
|
|
538
|
+
mpl_btn_clear.on_clicked(lambda _: _on_clear())
|
|
539
|
+
mpl_btn_peaks.on_clicked(lambda _: _on_peaks())
|
|
540
|
+
mpl_btn_export.on_clicked(lambda _: _on_export())
|
|
541
|
+
if mpl_btn_save:
|
|
542
|
+
mpl_btn_save.on_clicked(lambda _: _on_save())
|
|
543
|
+
|
|
544
|
+
state['_mpl_widgets'] = (sl_radius, sl_radius_minor, rb_angle,
|
|
545
|
+
mpl_btn_undo, mpl_btn_clear,
|
|
546
|
+
mpl_btn_peaks, mpl_btn_export,
|
|
547
|
+
mpl_btn_save)
|
|
548
|
+
|
|
549
|
+
print(f"Plotting {valid.sum()} valid pixels")
|
|
550
|
+
print(' Click on the phasor to place elliptical cursors. '
|
|
551
|
+
'First two define the decomposition line.')
|
|
552
|
+
|
|
553
|
+
if not notebook:
|
|
554
|
+
plt.show()
|
|
555
|
+
|
|
556
|
+
return state
|