flimkit-web-ui 0.1.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_web_ui/__init__.py +18 -0
- flimkit_web_ui/api.py +677 -0
- flimkit_web_ui/api_modes.py +329 -0
- flimkit_web_ui/bridge.py +286 -0
- flimkit_web_ui/core.js +408 -0
- flimkit_web_ui/fov.js +190 -0
- flimkit_web_ui/modes.js +375 -0
- flimkit_web_ui/page.html +469 -0
- flimkit_web_ui/server.py +114 -0
- flimkit_web_ui-0.1.0.dist-info/METADATA +107 -0
- flimkit_web_ui-0.1.0.dist-info/RECORD +15 -0
- flimkit_web_ui-0.1.0.dist-info/WHEEL +5 -0
- flimkit_web_ui-0.1.0.dist-info/entry_points.txt +2 -0
- flimkit_web_ui-0.1.0.dist-info/licenses/LICENSE.md +11 -0
- flimkit_web_ui-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import webbrowser
|
|
2
|
+
|
|
3
|
+
from flimkit.plugins import startup, tool
|
|
4
|
+
|
|
5
|
+
from . import server
|
|
6
|
+
|
|
7
|
+
FLIMKIT_PLUGIN_API = 1
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@startup(id='web_ui', order=900)
|
|
11
|
+
def launch_web_ui(app):
|
|
12
|
+
server.start(app)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@tool(id='open_web_ui', label='Open Web UI', menu='Tools', order=850)
|
|
16
|
+
def open_web_ui(app):
|
|
17
|
+
server.start(app)
|
|
18
|
+
webbrowser.open(server.url())
|
flimkit_web_ui/api.py
ADDED
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import io
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from . import bridge
|
|
10
|
+
|
|
11
|
+
EXTRA_VARS = {
|
|
12
|
+
'irf_fov.method': lambda app: app._irf_fov.sv_method,
|
|
13
|
+
'irf_fov.path': lambda app: app._irf_fov.sv_path,
|
|
14
|
+
'irf_st.method': lambda app: app._irf_st.sv_method,
|
|
15
|
+
'irf_st.path': lambda app: app._irf_st.sv_path,
|
|
16
|
+
'fov.tau_min': lambda app: app._fov_preview._sv_tau_min,
|
|
17
|
+
'fov.tau_max': lambda app: app._fov_preview._sv_tau_max,
|
|
18
|
+
'fov.gamma': lambda app: app._fov_preview._sv_gamma,
|
|
19
|
+
'fov.cmap': lambda app: app._fov_preview._sv_cmap,
|
|
20
|
+
'fov.show_decay': lambda app: app._fov_preview._bv_show_decay,
|
|
21
|
+
'fov.view': lambda app: app._fov_preview._sv_display_mode,
|
|
22
|
+
'fov.weighting': lambda app: app._fov_preview._sv_tau_weighting,
|
|
23
|
+
'ph.mode': lambda app: app._phasor_panel._mode_var,
|
|
24
|
+
'ph.radius': lambda app: app._phasor_panel._radius,
|
|
25
|
+
'ph.ratio': lambda app: app._phasor_panel._ratio,
|
|
26
|
+
'ph.filter_method': lambda app: app._phasor_panel._filter_method,
|
|
27
|
+
'ph.filter_sigma': lambda app: app._phasor_panel._filter_sigma,
|
|
28
|
+
'ph.filter_size': lambda app: app._phasor_panel._filter_size,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _tog(app, var, entry):
|
|
33
|
+
from flimkit.UI.utils import _tog as tog
|
|
34
|
+
tog(getattr(app, var), getattr(app, entry))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _tl_entries(app):
|
|
38
|
+
state = 'normal' if app.bv_tl_fix_tau.get() == True else 'disabled'
|
|
39
|
+
for e in (app._tl_tau1_e, app._tl_tau2_e, app._tl_tau3_e):
|
|
40
|
+
e.config(state=state)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
AFTER_SET = {
|
|
44
|
+
'sv_fov_analysis': lambda app: app._on_fov_analysis_changed(),
|
|
45
|
+
'bv_thr_fov': lambda app: _tog(app, 'bv_thr_fov', '_thr_fov_e'),
|
|
46
|
+
'bv_thr_st': lambda app: _tog(app, 'bv_thr_st', '_thr_st_e'),
|
|
47
|
+
'bv_batch_thr': lambda app: _tog(app, 'bv_batch_thr', '_batch_thr_e'),
|
|
48
|
+
'bv_tl_fix_tau': _tl_entries,
|
|
49
|
+
'sv_pipeline': lambda app: app._pipeline_changed(),
|
|
50
|
+
'bv_perpix': lambda app: app._perpix_toggled(),
|
|
51
|
+
'sv_batch_mode': lambda app: app._batch_mode_changed(),
|
|
52
|
+
'sv_ph_mode': lambda app: app._ph_mode_changed(),
|
|
53
|
+
'irf_fov.method': lambda app: app._irf_fov._update(),
|
|
54
|
+
'irf_st.method': lambda app: app._irf_st._update(),
|
|
55
|
+
'fov.show_decay': lambda app: app._fov_preview._toggle_decay(),
|
|
56
|
+
'fov.view': lambda app: app._fov_preview._on_display_mode_changed(),
|
|
57
|
+
'fov.weighting': lambda app: app._fov_preview._on_weighting_changed(),
|
|
58
|
+
'ph.mode': lambda app: app._phasor_panel._on_mode_change(),
|
|
59
|
+
'ph.radius': lambda app: app._phasor_panel._on_param_change(),
|
|
60
|
+
'ph.ratio': lambda app: app._phasor_panel._on_param_change(),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _var_table(app):
|
|
65
|
+
table = {}
|
|
66
|
+
for nam, var in app.state.__dict__.items():
|
|
67
|
+
if callable(getattr(var, 'get', None)) and callable(getattr(var, 'set', None)):
|
|
68
|
+
table[nam] = var
|
|
69
|
+
for nam, get in EXTRA_VARS.items():
|
|
70
|
+
try:
|
|
71
|
+
table[nam] = get(app)
|
|
72
|
+
except AttributeError:
|
|
73
|
+
pass
|
|
74
|
+
return table
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _coerce(var, value):
|
|
78
|
+
import tkinter as tk
|
|
79
|
+
if isinstance(var, tk.BooleanVar):
|
|
80
|
+
return value == True or str(value).lower() == 'true'
|
|
81
|
+
if isinstance(var, tk.IntVar):
|
|
82
|
+
return int(float(value))
|
|
83
|
+
if isinstance(var, tk.DoubleVar):
|
|
84
|
+
return float(value)
|
|
85
|
+
return '' if value is None else str(value)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def set_fields(app, values):
|
|
89
|
+
bridge.mark_action()
|
|
90
|
+
|
|
91
|
+
def do():
|
|
92
|
+
table = _var_table(app)
|
|
93
|
+
for key, value in values.items():
|
|
94
|
+
if key not in table:
|
|
95
|
+
raise ValueError('Unknown field ' + key)
|
|
96
|
+
var = table[key]
|
|
97
|
+
var.set(_coerce(var, value))
|
|
98
|
+
if key in AFTER_SET:
|
|
99
|
+
AFTER_SET[key](app)
|
|
100
|
+
|
|
101
|
+
bridge.on_ui(app, do)
|
|
102
|
+
return bridge.drain_notes()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def roi_panel(app):
|
|
106
|
+
return app._fov_preview._roi_analysis_panel or app._roi_analysis_panel
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _btn_state(btn):
|
|
110
|
+
return str(btn.cget('state')) == 'disabled'
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _btn_text(btn):
|
|
114
|
+
return str(btn.cget('text')).lstrip('▶ ').strip()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def expert_values(app):
|
|
118
|
+
from flimkit.UI.expert_settings import _EXPERT_DEFAULTS
|
|
119
|
+
from flimkit.UI.utils import _C
|
|
120
|
+
c = _C()
|
|
121
|
+
vals = dict(_EXPERT_DEFAULTS)
|
|
122
|
+
vals.update({
|
|
123
|
+
'binning_factor': c['binning_factor'],
|
|
124
|
+
'optimizer': c['Optimizer'],
|
|
125
|
+
'lm_restarts': c['lm_restarts'],
|
|
126
|
+
'de_population': c['de_population'],
|
|
127
|
+
'de_maxiter': c['de_maxiter'],
|
|
128
|
+
'n_workers': c['n_workers'],
|
|
129
|
+
'min_photons': c['MIN_PHOTONS_PERPIX'],
|
|
130
|
+
})
|
|
131
|
+
try:
|
|
132
|
+
from flimkit.utils.config_manager import cfg
|
|
133
|
+
vals.update(cfg.get_section('expert') or {})
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
vals.update(app._expert_overrides or {})
|
|
137
|
+
return vals
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _cursor_json(cur):
|
|
141
|
+
if cur.get('type', 'ellipse') == 'poly':
|
|
142
|
+
return {'type': 'poly', 'color': cur['color'], 'vertices': [[float(v[0]), float(v[1])] for v in cur['vertices']]}
|
|
143
|
+
return {'type': 'ellipse', 'color': cur['color'], 'g': float(cur['center_g']), 's': float(cur['center_s'])}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_state(app):
|
|
147
|
+
bridge.mark_poll()
|
|
148
|
+
|
|
149
|
+
def read():
|
|
150
|
+
from flimkit.UI.irf_widget import IRFWidget
|
|
151
|
+
from flimkit.utils import display
|
|
152
|
+
p = app._fov_preview
|
|
153
|
+
res = app._res
|
|
154
|
+
panel = roi_panel(app)
|
|
155
|
+
ph = app._phasor_panel
|
|
156
|
+
pb = app._proj_browser
|
|
157
|
+
fields = {}
|
|
158
|
+
for key, var in _var_table(app).items():
|
|
159
|
+
try:
|
|
160
|
+
fields[key] = var.get()
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
stack = p._series or p._zstack
|
|
164
|
+
mgr = p._roi_manager
|
|
165
|
+
rois = []
|
|
166
|
+
for iid in panel._tree.get_children():
|
|
167
|
+
rid = int(iid)
|
|
168
|
+
rois.append({'id': rid, 'values': [str(v) for v in panel._tree.item(iid, 'values')], 'color': mgr.get_color(rid)})
|
|
169
|
+
fit = res._fit_result or {}
|
|
170
|
+
try:
|
|
171
|
+
roi_defaults = app._get_roi_fit_params()
|
|
172
|
+
roi_defaults = {k: roi_defaults.get(k) for k in ('n_exp', 'tau_min', 'tau_max', 'cost_function')}
|
|
173
|
+
except Exception:
|
|
174
|
+
roi_defaults = {'n_exp': 1, 'tau_min': 0.1, 'tau_max': 25.0, 'cost_function': 'poisson'}
|
|
175
|
+
log = res.log.get('1.0', 'end')
|
|
176
|
+
buttons = {'fov': app._btn_fov, 'stitch': app._btn_st, 'phasor': app._btn_ph,
|
|
177
|
+
'batch': app._btn_batch, 'irf': app._btn_mirf}
|
|
178
|
+
progress = bridge.progress_state()
|
|
179
|
+
return {
|
|
180
|
+
'form': getattr(app, '_current_form', 'fov'),
|
|
181
|
+
'fields': fields,
|
|
182
|
+
'choices': {'irf': [list(c) for c in IRFWidget.CHOICES], 'cmap': list(display.COLORMAPS.keys())},
|
|
183
|
+
'run_labels': {k: _btn_text(b) for k, b in buttons.items()},
|
|
184
|
+
'busy': {k: _btn_state(b) for k, b in buttons.items()},
|
|
185
|
+
'running': any(_btn_state(b) for b in (app._btn_fov, app._btn_st, app._btn_ph)) or len(progress) > 0,
|
|
186
|
+
'progress': progress,
|
|
187
|
+
'preview_status': p._status.get(),
|
|
188
|
+
'results_status': res._status.get(),
|
|
189
|
+
'scale_controls': bool(p._ctrl_frame.grid_info()),
|
|
190
|
+
'z': {'i': p._z_i, 'n': len(stack), 'label': p._z_label.get()} if stack else None,
|
|
191
|
+
'summary': [[str(v) for v in res._tv.item(i, 'values')] for i in res._tv.get_children()],
|
|
192
|
+
'log_tail': log[-20000:],
|
|
193
|
+
'log_len': len(log),
|
|
194
|
+
'images': [str(x) for x in res._imgs],
|
|
195
|
+
'export_images': sorted(k for k, v in fit.items()
|
|
196
|
+
if isinstance(v, np.ndarray) and (v.ndim == 2 or (v.ndim == 3 and v.shape[2] == 3))),
|
|
197
|
+
'output_dir': str(res._output_dir or ''),
|
|
198
|
+
'npz_path': str(res._current_npz_path or ''),
|
|
199
|
+
'rois': rois,
|
|
200
|
+
'roi_selected': [int(i) for i in panel._tree.selection()],
|
|
201
|
+
'roi_mode': panel._current_mode.get(),
|
|
202
|
+
'roi_status': panel._status.get(),
|
|
203
|
+
'roi_fits': [list(k) for k in panel._last_fit_results.keys()],
|
|
204
|
+
'roi_can_fit': callable(getattr(panel, 'get_fit_params', None)),
|
|
205
|
+
'roi_defaults': roi_defaults,
|
|
206
|
+
'expert': expert_values(app),
|
|
207
|
+
'expert_active': bool(app._expert_overrides),
|
|
208
|
+
'phasor': {
|
|
209
|
+
'loaded': ph._real is not None,
|
|
210
|
+
'status': ph._status_var.get(),
|
|
211
|
+
'freq': float(ph._freq or 0.0),
|
|
212
|
+
'cursors': [_cursor_json(c) for c in ph._cursors],
|
|
213
|
+
'max_cursors': ph.max_cursors,
|
|
214
|
+
'has_fit': ph._last_fit_result is not None,
|
|
215
|
+
'peaks': [] if ph._peak_results is None else [
|
|
216
|
+
{'g': float(ph._peak_results['peak_g'][i]), 's': float(ph._peak_results['peak_s'][i]),
|
|
217
|
+
'tau': float(ph._peak_results['tau_phase'][i])}
|
|
218
|
+
for i in range(int(ph._peak_results['n_peaks']))],
|
|
219
|
+
},
|
|
220
|
+
'batch': {
|
|
221
|
+
'label': str(app._batch_mode_label.cget('text')),
|
|
222
|
+
'help': str(app._batch_io_help.cget('text')),
|
|
223
|
+
'beside': app.sv_batch_save_beside_preview.get(),
|
|
224
|
+
},
|
|
225
|
+
'project': {
|
|
226
|
+
'status': pb._sv_status.get(),
|
|
227
|
+
'items': [str(x) for x in pb._lb.get(0, 'end')],
|
|
228
|
+
'selected': [int(i) for i in pb._lb.curselection()],
|
|
229
|
+
},
|
|
230
|
+
'recent': [dict(e) for e in app._recent_files],
|
|
231
|
+
'figs': bridge.fig_revs(app),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
state = bridge.on_ui(app, read, timeout=8.0)
|
|
235
|
+
state['notes'] = bridge.drain_notes()
|
|
236
|
+
return state
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def list_dir(directory):
|
|
240
|
+
directory = os.path.abspath(os.path.expanduser(directory or '~'))
|
|
241
|
+
if os.path.isdir(directory) == False:
|
|
242
|
+
directory = os.path.expanduser('~')
|
|
243
|
+
entries = []
|
|
244
|
+
try:
|
|
245
|
+
with os.scandir(directory) as it:
|
|
246
|
+
for entry in it:
|
|
247
|
+
if entry.name.startswith('.'):
|
|
248
|
+
continue
|
|
249
|
+
try:
|
|
250
|
+
is_dir = entry.is_dir()
|
|
251
|
+
except OSError:
|
|
252
|
+
continue
|
|
253
|
+
entries.append({'name': entry.name, 'path': entry.path, 'is_dir': is_dir})
|
|
254
|
+
except OSError as exc:
|
|
255
|
+
return {'error': str(exc)}
|
|
256
|
+
entries.sort(key=lambda e: (e['is_dir'] == False, e['name'].lower()))
|
|
257
|
+
parent = os.path.dirname(directory)
|
|
258
|
+
return {'dir': directory, 'parent': parent if parent != directory else None, 'entries': entries}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _stem(app):
|
|
262
|
+
ptu = app._fov_preview._ptu_path
|
|
263
|
+
return Path(str(ptu)).stem if ptu else 'flimkit'
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _select(app, ids):
|
|
267
|
+
panel = roi_panel(app)
|
|
268
|
+
p = app._fov_preview
|
|
269
|
+
current = panel._tree.selection()
|
|
270
|
+
if current:
|
|
271
|
+
panel._tree.selection_remove(current)
|
|
272
|
+
if ids:
|
|
273
|
+
panel._tree.selection_set([str(i) for i in ids])
|
|
274
|
+
p._roi_manager.select_region(ids[0] if ids else None)
|
|
275
|
+
p._redraw_region_overlays()
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _capture(call, nam, mime):
|
|
279
|
+
fd, tmp = tempfile.mkstemp(suffix=Path(nam).suffix)
|
|
280
|
+
os.close(fd)
|
|
281
|
+
os.remove(tmp)
|
|
282
|
+
bridge.answer('asksaveasfilename', tmp)
|
|
283
|
+
call()
|
|
284
|
+
if os.path.exists(tmp) == False:
|
|
285
|
+
return {}
|
|
286
|
+
with open(tmp, encoding='utf-8') as f:
|
|
287
|
+
text = f.read()
|
|
288
|
+
os.remove(tmp)
|
|
289
|
+
return {'download': {'name': nam, 'mime': mime, 'text': text}}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _ids(args):
|
|
293
|
+
return [int(i) for i in args.get('ids', [])]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _need_idle(app):
|
|
297
|
+
if any(_btn_state(b) for b in (app._btn_fov, app._btn_st, app._btn_ph)) or bridge.progress_state():
|
|
298
|
+
raise ValueError('A task is already running.')
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def fit_rows(result):
|
|
302
|
+
summary = result['summary']
|
|
303
|
+
taus = list(summary.get('taus_ns', []))
|
|
304
|
+
amps = list(summary.get('amps', []))
|
|
305
|
+
chi2 = summary.get('reduced_chi2_tail')
|
|
306
|
+
rows = [['τ' + str(i + 1), '%.4f' % t, 'ns'] for i, t in enumerate(taus)]
|
|
307
|
+
rows += [['A' + str(i + 1) + ' (amplitude)', '%.4f' % a, ''] for i, a in enumerate(amps)]
|
|
308
|
+
if taus and amps and np.sum(amps) > 0:
|
|
309
|
+
rows.append(['τ_mean (amplitude-weighted)', '%.4f' % float(np.dot(taus, amps) / np.sum(amps)), 'ns'])
|
|
310
|
+
if chi2 is not None:
|
|
311
|
+
rows.append(['χ²_r (tail)', '%.4f' % chi2, ''])
|
|
312
|
+
return {'name': result['region_name'], 'irf_source': result['irf_source'], 'rows': rows}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def fit_png(result):
|
|
316
|
+
from matplotlib.backends.backend_agg import FigureCanvasAgg
|
|
317
|
+
from matplotlib.figure import Figure
|
|
318
|
+
decay = np.asarray(result['decay'])
|
|
319
|
+
time_ns = np.asarray(result['time_ns'])
|
|
320
|
+
irf = result['irf_prompt']
|
|
321
|
+
model = result['summary'].get('model')
|
|
322
|
+
fig = Figure(figsize=(6.2, 4.2), facecolor='#2b2b2b')
|
|
323
|
+
FigureCanvasAgg(fig)
|
|
324
|
+
ax_d = fig.add_subplot(4, 1, (1, 3))
|
|
325
|
+
ax_r = fig.add_subplot(4, 1, 4, sharex=ax_d)
|
|
326
|
+
for ax in (ax_d, ax_r):
|
|
327
|
+
ax.set_facecolor('#1e1e1e')
|
|
328
|
+
ax.tick_params(colors='white', labelsize=8)
|
|
329
|
+
ax_d.semilogy(time_ns, decay, 'o-', color='steelblue', linewidth=1.2, markersize=2, label='Decay', alpha=0.8)
|
|
330
|
+
if irf is not None and np.max(irf) > 0:
|
|
331
|
+
irf = np.asarray(irf)
|
|
332
|
+
irf_sc = (irf / np.max(irf)) * decay.max() * 0.15
|
|
333
|
+
ax_d.semilogy(time_ns[:len(irf)], np.maximum(irf_sc, 1e-2), color='orange', linewidth=1.5,
|
|
334
|
+
label='IRF (' + str(result['irf_source']) + ')', alpha=0.7)
|
|
335
|
+
if model is not None and len(model) == len(decay):
|
|
336
|
+
model = np.asarray(model)
|
|
337
|
+
ax_d.semilogy(time_ns, model, color='red', linewidth=2.0, label='Fit', alpha=0.9)
|
|
338
|
+
with np.errstate(invalid='ignore', divide='ignore'):
|
|
339
|
+
resid = np.where(model > 0, (decay - model) / np.sqrt(model), 0.0)
|
|
340
|
+
ax_r.plot(time_ns, resid, color='steelblue', linewidth=0.9)
|
|
341
|
+
ax_r.axhline(0, color='red', linewidth=1.0, linestyle='--', alpha=0.7)
|
|
342
|
+
ax_d.legend(fontsize=7, loc='upper right', labelcolor='white', facecolor='#333333', edgecolor='#555555')
|
|
343
|
+
ax_d.set_title(str(result['region_name']), fontsize=9, color='white')
|
|
344
|
+
ax_r.set_xlabel('Time (ns)', color='white', fontsize=8)
|
|
345
|
+
fig.tight_layout(pad=0.8)
|
|
346
|
+
buf = io.BytesIO()
|
|
347
|
+
fig.savefig(buf, format='png', facecolor=fig.get_facecolor())
|
|
348
|
+
return buf.getvalue()
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def fit_result(app, src, ids):
|
|
352
|
+
def get():
|
|
353
|
+
if src == 'phasor':
|
|
354
|
+
return app._phasor_panel._last_fit_result
|
|
355
|
+
return roi_panel(app)._last_fit_results.get(tuple(sorted(ids)))
|
|
356
|
+
result = bridge.on_ui(app, get)
|
|
357
|
+
if result is None:
|
|
358
|
+
raise ValueError('No fit result cached for this selection yet.')
|
|
359
|
+
return result
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def result_image(app, i):
|
|
363
|
+
path = bridge.on_ui(app, lambda: Path(app._res._imgs[i]))
|
|
364
|
+
if path.suffix.lower() == '.png':
|
|
365
|
+
return path.read_bytes()
|
|
366
|
+
from PIL import Image
|
|
367
|
+
buf = io.BytesIO()
|
|
368
|
+
Image.open(path).convert('RGB').save(buf, format='PNG')
|
|
369
|
+
return buf.getvalue()
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# ---------- Single FOV, display, results ----------
|
|
373
|
+
|
|
374
|
+
def act_run_fov(app, args):
|
|
375
|
+
_need_idle(app)
|
|
376
|
+
if app.sv_fov_analysis.get() == 'zstack':
|
|
377
|
+
if Path(app.sv_zstack_dir.get().strip() or '.missing').is_dir() == False:
|
|
378
|
+
raise ValueError('Please select a valid z-stack folder.')
|
|
379
|
+
else:
|
|
380
|
+
ptu = app.sv_ptu.get().strip()
|
|
381
|
+
if ptu == '' or Path(ptu).exists() == False:
|
|
382
|
+
raise ValueError('Please select a valid PTU file.')
|
|
383
|
+
app._run_fov()
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def act_cancel(app, args):
|
|
387
|
+
bridge.cancel_all()
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def act_switch_form(app, args):
|
|
391
|
+
form = str(args.get('form', 'fov'))
|
|
392
|
+
if form not in ('fov', 'stitch', 'phasor', 'batch', 'irf'):
|
|
393
|
+
raise ValueError('Unknown mode ' + form)
|
|
394
|
+
app._switch_form(form)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def act_auto_scale(app, args):
|
|
398
|
+
app._fov_preview._auto_detect_scale()
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def act_update_display(app, args):
|
|
402
|
+
app._fov_preview._update_flim_display()
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def act_z(app, args):
|
|
406
|
+
app._fov_preview._on_z_slider(int(args.get('i', 0)))
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def act_clear_log(app, args):
|
|
410
|
+
app._res._clear_log()
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def act_save_log(app, args):
|
|
414
|
+
text = app._res.log.get('1.0', 'end')
|
|
415
|
+
return {'download': {'name': _stem(app) + '_log.txt', 'mime': 'text/plain', 'text': text}}
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def act_export_summary_csv(app, args):
|
|
419
|
+
tv = app._res._tv
|
|
420
|
+
rows = [tv.item(i, 'values') for i in tv.get_children()]
|
|
421
|
+
if not rows:
|
|
422
|
+
raise ValueError('No summary data to export.')
|
|
423
|
+
buf = io.StringIO()
|
|
424
|
+
writer = csv.writer(buf)
|
|
425
|
+
writer.writerow(['Parameter', 'Value', 'Unit'])
|
|
426
|
+
writer.writerows(rows)
|
|
427
|
+
return {'download': {'name': _stem(app) + '_summed_fit.csv', 'mime': 'text/csv', 'text': buf.getvalue()}}
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def act_export_images(app, args):
|
|
431
|
+
fit = app._res._fit_result or {}
|
|
432
|
+
selected = {k: fit[k] for k in args.get('images', []) if k in fit}
|
|
433
|
+
if not selected:
|
|
434
|
+
raise ValueError('Please select at least one image to export.')
|
|
435
|
+
path = str(args.get('path', '') or '').strip() or str(app._res._output_dir or '')
|
|
436
|
+
if path == '':
|
|
437
|
+
raise ValueError('Please select an export directory.')
|
|
438
|
+
app._export_images(selected, path,
|
|
439
|
+
with_scalebar=args.get('scalebar', True) == True,
|
|
440
|
+
with_annotations=args.get('annotations', True) == True,
|
|
441
|
+
format=args.get('format', 'png'),
|
|
442
|
+
fit_result=fit)
|
|
443
|
+
return {'message': 'Results exported to ' + path}
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def act_save_session(app, args):
|
|
447
|
+
folder = str(args.get('path', '')).strip()
|
|
448
|
+
if folder == '':
|
|
449
|
+
raise ValueError('Choose a folder to save the session into.')
|
|
450
|
+
bridge.answer('askdirectory', folder)
|
|
451
|
+
bridge.answer('askyesno', True)
|
|
452
|
+
app._save_npz_quick(str(app._res._output_dir or ''))
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def act_save_npz_as(app, args):
|
|
456
|
+
path = str(args.get('path', '')).strip()
|
|
457
|
+
if path == '':
|
|
458
|
+
raise ValueError('Enter the .npz file path to save to.')
|
|
459
|
+
bridge.answer('asksaveasfilename', path)
|
|
460
|
+
app._menu_save_npz_as()
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def act_load_session(app, args):
|
|
464
|
+
path = str(args.get('path', '')).strip()
|
|
465
|
+
if path == '' or Path(path).is_file() == False:
|
|
466
|
+
raise ValueError('Choose a .npz session file.')
|
|
467
|
+
app._load_fitted_data_from_file(path)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def act_menu_reset(app, args):
|
|
471
|
+
bridge.answer('askyesno', True)
|
|
472
|
+
app._menu_reset()
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
# ---------- ROIs ----------
|
|
476
|
+
|
|
477
|
+
def act_roi_mode(app, args):
|
|
478
|
+
roi_panel(app)._set_mode(str(args.get('mode', 'select')))
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def act_roi_add(app, args):
|
|
482
|
+
tool = str(args.get('tool', ''))
|
|
483
|
+
if tool not in ('rect', 'ellipse', 'polygon', 'freehand'):
|
|
484
|
+
raise ValueError('Unknown ROI tool ' + tool)
|
|
485
|
+
p = app._fov_preview
|
|
486
|
+
p._draw_coords = [[float(x), float(y)] for x, y in args.get('coords', [])]
|
|
487
|
+
p._finalize_drawing(tool)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def act_roi_select(app, args):
|
|
491
|
+
_select(app, _ids(args))
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def act_roi_delete(app, args):
|
|
495
|
+
p = app._fov_preview
|
|
496
|
+
for rid in _ids(args):
|
|
497
|
+
p._roi_manager.remove_region(rid)
|
|
498
|
+
p._redraw_region_overlays()
|
|
499
|
+
p._save_regions_update()
|
|
500
|
+
roi_panel(app)._refresh_region_list()
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def act_roi_rename(app, args):
|
|
504
|
+
name = str(args.get('name', '')).strip()
|
|
505
|
+
if name == '':
|
|
506
|
+
raise ValueError('Enter a name.')
|
|
507
|
+
p = app._fov_preview
|
|
508
|
+
p._roi_manager.update_region(int(args['id']), name=name)
|
|
509
|
+
p._save_regions_update()
|
|
510
|
+
roi_panel(app)._refresh_region_list()
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def act_roi_clear(app, args):
|
|
514
|
+
roi_panel(app)._clear_all_regions()
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def act_roi_import(app, args):
|
|
518
|
+
path = str(args.get('path', '')).strip()
|
|
519
|
+
if path == '':
|
|
520
|
+
raise ValueError('Choose a GeoJSON file.')
|
|
521
|
+
bridge.answer('askopenfilename', path)
|
|
522
|
+
roi_panel(app)._import_rois_geojson()
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def act_roi_export_csv(app, args):
|
|
526
|
+
return _capture(roi_panel(app)._export_all_rois_csv, _stem(app) + '_roi_data.csv', 'text/csv')
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def act_roi_export_geojson(app, args):
|
|
530
|
+
panel = roi_panel(app)
|
|
531
|
+
ids = _ids(args)
|
|
532
|
+
if ids:
|
|
533
|
+
_select(app, ids[:1])
|
|
534
|
+
return _capture(panel._export_selected_region, _stem(app) + '_roi_' + str(ids[0]) + '.geojson', 'application/geo+json')
|
|
535
|
+
return _capture(panel._export_all_rois_geojson, _stem(app) + '_all_rois.geojson', 'application/geo+json')
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _fit_opts(args):
|
|
539
|
+
opts = {
|
|
540
|
+
'n_exp': int(args.get('n_exp', 1)),
|
|
541
|
+
'tau_min': float(args.get('tau_min', 0.1)),
|
|
542
|
+
'tau_max': float(args.get('tau_max', 25.0)),
|
|
543
|
+
'cost_function': str(args.get('cost_function', 'poisson')),
|
|
544
|
+
}
|
|
545
|
+
if opts['tau_min'] <= 0 or opts['tau_max'] <= opts['tau_min']:
|
|
546
|
+
raise ValueError('Need 0 < τ_min < τ_max.')
|
|
547
|
+
return opts
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def act_roi_fit(app, args):
|
|
551
|
+
ids = _ids(args)
|
|
552
|
+
if not ids:
|
|
553
|
+
raise ValueError('Select a region in the list first.')
|
|
554
|
+
bridge.answer('roi_opts', _fit_opts(args))
|
|
555
|
+
_select(app, ids)
|
|
556
|
+
roi_panel(app)._fit_roi_decay()
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def act_fit_rows(app, args):
|
|
560
|
+
getter = app._phasor_panel._last_fit_result if args.get('src') == 'phasor' \
|
|
561
|
+
else roi_panel(app)._last_fit_results.get(tuple(sorted(_ids(args))))
|
|
562
|
+
if getter is None:
|
|
563
|
+
raise ValueError('No fit result cached for this selection yet.')
|
|
564
|
+
return fit_rows(getter)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
# ---------- expert settings ----------
|
|
568
|
+
|
|
569
|
+
def _expert_parse(raw):
|
|
570
|
+
from flimkit.interactive import parse_exclude_ns
|
|
571
|
+
|
|
572
|
+
def num(key, default, cast):
|
|
573
|
+
s = str(raw.get(key) if raw.get(key) is not None else '').strip()
|
|
574
|
+
return cast(s) if s != '' else default
|
|
575
|
+
|
|
576
|
+
ch = str(raw.get('channels', '') or '').strip()
|
|
577
|
+
excl = str(raw.get('exclude_ns', '') or '').strip()
|
|
578
|
+
parse_exclude_ns(excl or None)
|
|
579
|
+
return {
|
|
580
|
+
'optimizer': str(raw.get('optimizer', 'de')),
|
|
581
|
+
'de_population': num('de_population', 30, int),
|
|
582
|
+
'de_maxiter': num('de_maxiter', 5000, int),
|
|
583
|
+
'lm_restarts': num('lm_restarts', 8, int),
|
|
584
|
+
'binning_factor': num('binning_factor', 1, int),
|
|
585
|
+
'n_workers': num('n_workers', -1, int),
|
|
586
|
+
'min_photons': num('min_photons', 10, int),
|
|
587
|
+
'cost_function': str(raw.get('cost_function', 'poisson')),
|
|
588
|
+
'channels': int(ch) if ch.isdigit() else (None if ch == '' else ch),
|
|
589
|
+
'irf_fwhm': num('irf_fwhm', None, float),
|
|
590
|
+
'irf_align': str(raw.get('irf_align', 'steepest_rise')),
|
|
591
|
+
'irf_shift_bins': num('irf_shift_bins', 2, int),
|
|
592
|
+
'align_irf': raw.get('align_irf') == True,
|
|
593
|
+
'free_tau_perpixel': raw.get('free_tau_perpixel') == True,
|
|
594
|
+
'pileup_in_model': raw.get('pileup_in_model') == True,
|
|
595
|
+
'bg_in_model': raw.get('bg_in_model') == True,
|
|
596
|
+
'fit_t0': raw.get('fit_t0') == True,
|
|
597
|
+
'fit_start_ns': num('fit_start_ns', None, float),
|
|
598
|
+
'fit_end_ns': num('fit_end_ns', None, float),
|
|
599
|
+
'exclude_ns': excl,
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _expert_apply(app, result):
|
|
604
|
+
from flimkit.UI.expert_settings import _EXPERT_DEFAULTS
|
|
605
|
+
from flimkit.utils.config_manager import cfg
|
|
606
|
+
is_default = all(result.get(k) == v for k, v in _EXPERT_DEFAULTS.items())
|
|
607
|
+
app._expert_overrides = {} if is_default == True else result
|
|
608
|
+
cfg.update_section('expert', result)
|
|
609
|
+
browser = getattr(app, '_proj_browser', None)
|
|
610
|
+
if browser and browser._project:
|
|
611
|
+
browser._project.config['expert'] = result
|
|
612
|
+
browser._project.save()
|
|
613
|
+
cfg.load_project_overrides(browser._project.config)
|
|
614
|
+
app._update_expert_banners()
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def act_expert_save(app, args):
|
|
618
|
+
_expert_apply(app, _expert_parse(args.get('values', {})))
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def act_expert_reset(app, args):
|
|
622
|
+
from flimkit.UI.expert_settings import _EXPERT_DEFAULTS
|
|
623
|
+
_expert_apply(app, dict(_EXPERT_DEFAULTS))
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
ACTIONS = {
|
|
627
|
+
'run_fov': act_run_fov,
|
|
628
|
+
'cancel': act_cancel,
|
|
629
|
+
'switch_form': act_switch_form,
|
|
630
|
+
'auto_scale': act_auto_scale,
|
|
631
|
+
'update_display': act_update_display,
|
|
632
|
+
'z': act_z,
|
|
633
|
+
'clear_log': act_clear_log,
|
|
634
|
+
'save_log': act_save_log,
|
|
635
|
+
'export_summary_csv': act_export_summary_csv,
|
|
636
|
+
'export_images': act_export_images,
|
|
637
|
+
'save_session': act_save_session,
|
|
638
|
+
'save_npz_as': act_save_npz_as,
|
|
639
|
+
'load_session': act_load_session,
|
|
640
|
+
'menu_reset': act_menu_reset,
|
|
641
|
+
'roi_mode': act_roi_mode,
|
|
642
|
+
'roi_add': act_roi_add,
|
|
643
|
+
'roi_select': act_roi_select,
|
|
644
|
+
'roi_delete': act_roi_delete,
|
|
645
|
+
'roi_rename': act_roi_rename,
|
|
646
|
+
'roi_clear': act_roi_clear,
|
|
647
|
+
'roi_import': act_roi_import,
|
|
648
|
+
'roi_export_csv': act_roi_export_csv,
|
|
649
|
+
'roi_export_geojson': act_roi_export_geojson,
|
|
650
|
+
'roi_fit': act_roi_fit,
|
|
651
|
+
'fit_rows': act_fit_rows,
|
|
652
|
+
'expert_save': act_expert_save,
|
|
653
|
+
'expert_reset': act_expert_reset,
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def run_action(app, name, args):
|
|
658
|
+
from . import api_modes
|
|
659
|
+
bridge.mark_action()
|
|
660
|
+
off_ui = api_modes.OFF_UI.get(name)
|
|
661
|
+
fn = off_ui or ACTIONS.get(name) or api_modes.ACTIONS.get(name)
|
|
662
|
+
if fn is None:
|
|
663
|
+
raise ValueError('Unknown action ' + str(name))
|
|
664
|
+
|
|
665
|
+
def do():
|
|
666
|
+
try:
|
|
667
|
+
return fn(app, args) or {}
|
|
668
|
+
finally:
|
|
669
|
+
bridge.clear_answers()
|
|
670
|
+
|
|
671
|
+
try:
|
|
672
|
+
out = do() if off_ui is not None else bridge.on_ui(app, do)
|
|
673
|
+
except Exception:
|
|
674
|
+
bridge.clear_answers()
|
|
675
|
+
raise
|
|
676
|
+
out['notes'] = bridge.drain_notes()
|
|
677
|
+
return out
|