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
flimkit/UI/utils.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import re
|
|
3
|
+
import time
|
|
4
|
+
import threading
|
|
5
|
+
import tkinter as tk
|
|
6
|
+
from tkinter import ttk, filedialog, scrolledtext
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
import numpy as np
|
|
10
|
+
from flimkit.UI.fit_help import help_button
|
|
11
|
+
from flimkit.utils.config_snapshot import _C
|
|
12
|
+
from flimkit.utils.session import (
|
|
13
|
+
_reconstruct_dict_from_session,
|
|
14
|
+
_safe_array_from_json,
|
|
15
|
+
_parse_summary,
|
|
16
|
+
)
|
|
17
|
+
try:
|
|
18
|
+
from tkinterdnd2 import DND_FILES, DND_TEXT
|
|
19
|
+
HAS_DND = True
|
|
20
|
+
except ImportError:
|
|
21
|
+
HAS_DND = False
|
|
22
|
+
|
|
23
|
+
def _enable_dnd(root):
|
|
24
|
+
if not HAS_DND:
|
|
25
|
+
return False
|
|
26
|
+
try:
|
|
27
|
+
from tkinterdnd2 import TkinterDnD
|
|
28
|
+
TkinterDnD._require(root)
|
|
29
|
+
return True
|
|
30
|
+
except Exception:
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
class _Redirect:
|
|
34
|
+
|
|
35
|
+
def __init__(self, widget: scrolledtext.ScrolledText, buf: list, root=None, is_stderr=False):
|
|
36
|
+
self.widget = widget
|
|
37
|
+
self.buf = buf
|
|
38
|
+
self.root = root
|
|
39
|
+
self._is_stderr = is_stderr
|
|
40
|
+
self._batch = []
|
|
41
|
+
self._batch_size = 5000
|
|
42
|
+
self._last_flush = time.time()
|
|
43
|
+
self._flush_interval = 0.5
|
|
44
|
+
|
|
45
|
+
def write(self, text: str):
|
|
46
|
+
if not text:
|
|
47
|
+
return
|
|
48
|
+
self.buf.append(text)
|
|
49
|
+
self._batch.append(text)
|
|
50
|
+
if self._is_stderr:
|
|
51
|
+
try:
|
|
52
|
+
from flimkit.utils.crash_handler import log_event
|
|
53
|
+
log_event(f'STDERR: {text.rstrip()}', level='warning')
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
should_flush = False
|
|
57
|
+
if len(''.join(self._batch)) >= self._batch_size:
|
|
58
|
+
should_flush = True
|
|
59
|
+
elif time.time() - self._last_flush >= self._flush_interval:
|
|
60
|
+
should_flush = True
|
|
61
|
+
if should_flush:
|
|
62
|
+
self._flush_batch()
|
|
63
|
+
|
|
64
|
+
def _flush_batch(self):
|
|
65
|
+
if not self._batch:
|
|
66
|
+
return
|
|
67
|
+
text = ''.join(self._batch)
|
|
68
|
+
self._batch.clear()
|
|
69
|
+
if self.root:
|
|
70
|
+
self.root.after(0, self._update_widget, text)
|
|
71
|
+
else:
|
|
72
|
+
self._update_widget(text)
|
|
73
|
+
|
|
74
|
+
def _update_widget(self, text):
|
|
75
|
+
try:
|
|
76
|
+
self.widget.configure(state='normal')
|
|
77
|
+
self.widget.insert(tk.END, text)
|
|
78
|
+
self.widget.see(tk.END)
|
|
79
|
+
self.widget.configure(state='disabled')
|
|
80
|
+
self.widget.update_idletasks()
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
self._last_flush = time.time()
|
|
84
|
+
|
|
85
|
+
def flush(self):
|
|
86
|
+
self._flush_batch()
|
|
87
|
+
|
|
88
|
+
class _FileRedirect:
|
|
89
|
+
|
|
90
|
+
def __init__(self, filepath: str, buf: list):
|
|
91
|
+
self.filepath = filepath
|
|
92
|
+
self.buf = buf
|
|
93
|
+
self._file = None
|
|
94
|
+
try:
|
|
95
|
+
self._file = open(filepath, 'w', buffering=1)
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
def write(self, text: str):
|
|
100
|
+
if not text:
|
|
101
|
+
return
|
|
102
|
+
self.buf.append(text)
|
|
103
|
+
if self._file:
|
|
104
|
+
try:
|
|
105
|
+
self._file.write(text)
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
def flush(self):
|
|
110
|
+
if self._file:
|
|
111
|
+
try:
|
|
112
|
+
self._file.flush()
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
def close(self):
|
|
117
|
+
if self._file:
|
|
118
|
+
try:
|
|
119
|
+
self._file.close()
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
self._file = None
|
|
123
|
+
|
|
124
|
+
class _FileTailer:
|
|
125
|
+
|
|
126
|
+
def __init__(self, filepath: str, widget: scrolledtext.ScrolledText, update_interval_ms: int = 200):
|
|
127
|
+
self.filepath = filepath
|
|
128
|
+
self.widget = widget
|
|
129
|
+
self.update_interval_ms = update_interval_ms
|
|
130
|
+
self._file = None
|
|
131
|
+
self._last_pos = 0
|
|
132
|
+
self._running = False
|
|
133
|
+
|
|
134
|
+
def start(self, root):
|
|
135
|
+
self._running = True
|
|
136
|
+
self._poll_file(root)
|
|
137
|
+
|
|
138
|
+
def _poll_file(self, root):
|
|
139
|
+
if not self._running:
|
|
140
|
+
return
|
|
141
|
+
try:
|
|
142
|
+
if not Path(self.filepath).exists():
|
|
143
|
+
root.after(self.update_interval_ms, lambda: self._poll_file(root))
|
|
144
|
+
return
|
|
145
|
+
with open(self.filepath, 'r') as f:
|
|
146
|
+
f.seek(self._last_pos)
|
|
147
|
+
new_content = f.read()
|
|
148
|
+
self._last_pos = f.tell()
|
|
149
|
+
if new_content:
|
|
150
|
+
self.widget.configure(state='normal')
|
|
151
|
+
self.widget.insert(tk.END, new_content)
|
|
152
|
+
self.widget.see(tk.END)
|
|
153
|
+
self.widget.configure(state='disabled')
|
|
154
|
+
self.widget.update_idletasks()
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
root.after(self.update_interval_ms, lambda: self._poll_file(root))
|
|
158
|
+
|
|
159
|
+
def stop(self):
|
|
160
|
+
self._running = False
|
|
161
|
+
|
|
162
|
+
PAD = dict(padx=8, pady=4)
|
|
163
|
+
|
|
164
|
+
from flimkit.formats import file_dialog_filetypes
|
|
165
|
+
FLIM_FILETYPES = file_dialog_filetypes() + [('All', '*.*')]
|
|
166
|
+
|
|
167
|
+
def _browse_file(var, title='Select file', filetypes=(('All', '*.*'),)):
|
|
168
|
+
p = filedialog.askopenfilename(title=title, filetypes=filetypes)
|
|
169
|
+
if p:
|
|
170
|
+
var.set(p)
|
|
171
|
+
|
|
172
|
+
def _browse_dir(var, title='Select directory'):
|
|
173
|
+
p = filedialog.askdirectory(title=title)
|
|
174
|
+
if p:
|
|
175
|
+
var.set(p)
|
|
176
|
+
|
|
177
|
+
def _row(parent, label, var, row, browse_fn, width=45, state='normal'):
|
|
178
|
+
ttk.Label(parent, text=label).grid(
|
|
179
|
+
row=row, column=0, sticky='e', padx=6, pady=3)
|
|
180
|
+
e = ttk.Entry(parent, textvariable=var, width=width, state=state)
|
|
181
|
+
e.grid(row=row, column=1, sticky='ew', padx=4, pady=3)
|
|
182
|
+
ttk.Button(parent, text='Browse...', command=browse_fn).grid(
|
|
183
|
+
row=row, column=2, padx=4, pady=3)
|
|
184
|
+
if HAS_DND:
|
|
185
|
+
try:
|
|
186
|
+
def _drop_handler(event):
|
|
187
|
+
data = event.data.strip()
|
|
188
|
+
if data.startswith('{') and data.endswith('}'):
|
|
189
|
+
data = data[1:-1]
|
|
190
|
+
var.set(data)
|
|
191
|
+
e.drop_target_register(DND_FILES, DND_TEXT)
|
|
192
|
+
e.dnd_bind('<<Drop>>', _drop_handler)
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
return e
|
|
196
|
+
|
|
197
|
+
def _section(parent, text: str, help_topic: Optional[str] = None) -> ttk.LabelFrame:
|
|
198
|
+
lf = ttk.LabelFrame(parent, text=f' {text} ', padding=(10, 6))
|
|
199
|
+
if help_topic:
|
|
200
|
+
holder = ttk.Frame(lf)
|
|
201
|
+
ttk.Label(holder, text=f' {text} ').pack(side='left')
|
|
202
|
+
help_button(holder, help_topic).pack(side='left', padx=(0, 6))
|
|
203
|
+
lf.configure(labelwidget=holder)
|
|
204
|
+
return lf
|
|
205
|
+
|
|
206
|
+
def _tog(bvar: tk.BooleanVar, entry: ttk.Entry):
|
|
207
|
+
entry.configure(state='normal' if bvar.get() else 'disabled')
|
|
208
|
+
|
|
209
|
+
def _flt(sv: tk.StringVar) -> Optional[float]:
|
|
210
|
+
v = sv.get().strip()
|
|
211
|
+
return float(v) if v and v.lower() != 'none' else None
|
|
212
|
+
|
|
213
|
+
def _thresh(bvar: tk.BooleanVar, sv: tk.StringVar):
|
|
214
|
+
if not bvar.get():
|
|
215
|
+
return None
|
|
216
|
+
v = sv.get().strip()
|
|
217
|
+
return int(v) if v else None
|
flimkit/__init__.py
ADDED
|
File without changes
|
flimkit/_version.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
__version__ = '0.12.0'
|
|
2
|
+
fitter_version = '19'
|
|
3
|
+
|
|
4
|
+
roadmap = '''Flim program roadmap:
|
|
5
|
+
Version history:
|
|
6
|
+
0.1.0 -> Initial version with basic FLIM fitting functionality
|
|
7
|
+
0.1.1 -> Added command line interface (CLI) for single FOV fitting
|
|
8
|
+
0.1.2 -> Added support for IRF estimation from XLSX files
|
|
9
|
+
0.1.3 -> Added some user prompts and created defaults
|
|
10
|
+
0.1.4 -> Updated interactive prompts and added some error handling
|
|
11
|
+
Will be updated to 0.2.0 when ROI stitching works
|
|
12
|
+
0.3.0 -> ROI stiching + fitting
|
|
13
|
+
0.4.0 -> batch processing of multiple ROIs or FOVs
|
|
14
|
+
0.5.0 -> tbd
|
|
15
|
+
Need to add Phasor once added to the codebase, and update version to 1.0. And added some tests.
|
|
16
|
+
0.8.0 -> added per-tile machine IRF fitting workflow, updated docs and examples
|
|
17
|
+
0.8.1 -> added some error handling and user prompts to per-tile machine IRF workflow
|
|
18
|
+
0.8.2 -> added some more error handling and user prompts to per-tile machine IRF workflow, and updated example notebook
|
|
19
|
+
0.9.0 -> made flim fitted image visible in GUI
|
|
20
|
+
0.9.1 -> added session restoration in the GUI
|
|
21
|
+
0.9.2 -> added some error handling and begin session restoration for roi analysis panel in the GUI
|
|
22
|
+
0.9.3 -> complete session restoration for roi analysis panel
|
|
23
|
+
0.9.5 -> selectable ROIs in FLIM image in GUI and export of region stats
|
|
24
|
+
0.9.7 -> added FRET (using PhasorPy)
|
|
25
|
+
0.9.8 -> added pile-up correction function
|
|
26
|
+
0.9.9 -> UI fixes
|
|
27
|
+
0.9.10 -> fixed GPU tests
|
|
28
|
+
0.9.11 -> updated PhasorPy calls to match latest version (0.10.0) and added phasor filtering options
|
|
29
|
+
0.9.12 -> changed masking to use cellpose-SAM
|
|
30
|
+
0.9.13 -> added Gaussian and Lorentzian lifetime distribution fitting (Lakowicz §4.11.2)
|
|
31
|
+
0.9.14 -> bug fixes galore
|
|
32
|
+
0.9.15 -> new file format support: ISS (TD-FLIM .TAGTIME/.TAGCHANNEL/.TAGDECAY, FD-FLIM phasor .ifli; see issue #19), Becker & Hickl .sdt (SPC TCSPC FLIM, FIFO-image histograms; decoder written from B&H SPCM file-structure docs) and Photonscore .photons (LINCam D7 container; pure-Python, no native dependency, validated bit-exact vs the Photonscore SDK; see issue #20)
|
|
33
|
+
0.9.16 -> fixed pile-up correction: Coates was being fed the photon/record count instead of the excitation-pulse count (N_sync), which destroyed the decay tail and mis-reported pile-up (0.07% read as 97%). N_sync is now taken from the true sync count per format, and Coates refuses any count that cannot be one.
|
|
34
|
+
0.9.17 -> user-settable fit window + exclusion bands (--fit-start-ns/--fit-end-ns/--exclude-ns), so reflection peaks mid-decay can be dropped from the fit; the per-pixel path now honours the window at all (it previously projected over every bin, IRF rise and artefacts included). Pile-up can now go in the forward model (n_sync=) instead of correcting the data, which keeps the fit Poisson.
|
|
35
|
+
0.9.18 -> added the n-exponential tail fit: bare exponentials fitted past the decay peak with no IRF and no reconvolution, y(t) = sum A[i] exp(-(t-t0)/tau[i]) + Bkgr. Reports the Leica quantities (per-component intensities I[k] = A[k]tau[k], I_sum, A_sum, t0) alongside the usual amplitude- and intensity-weighted mean lifetimes. t0 is pinned to the decay peak by default because it is strongly correlated with the amplitudes; free it from expert settings or --fit-t0. Available in FOV, batch and stitch modes, and as --fit-model tail on the CLI.
|
|
36
|
+
0.9.19 -> multidimensional (z-stack and/or timelapse) tile stitching: a fourth Stitch pipeline that walks every (timepoint, z) plane, stitches the overlapping tiles for that plane and fits it, so overlapping positions are no longer treated as separate fields of view. One decay is pooled across the series and the lifetimes held fixed for every plane, so amplitudes stay comparable between timepoints. Tile positions come from the XLIF/LIF when one is given and are then refined against the tile overlap, which matters because the nominal stage coordinates can be out by tens of pixels; without metadata they are recovered from the overlap alone. Results browse with a plane slider in the FOV preview.
|
|
37
|
+
0.10.0 -> add-on system: analysis tools, file formats, format sniffers and phasor filters register themselves through a flimkit.plugins registry instead of being hard-coded, and FLIMKit's own tools go through the same registry a third party would use. Plugins load from ~/.flimkit/plugins (off until you turn it on), from folders named in the config or FLIMKIT_PLUGIN_PATH, and from installed packages that declare a flimkit.plugins entry point. A plugin that raises on import has its registrations rolled back and cannot take the app down; Help > Plugins reports what loaded and what failed, and turns individual plugins off. FLIMKIT_NO_PLUGINS=1 or --no-plugins skips the lot for a reproducible baseline. The v1 hook signatures are frozen and a fixture plugin written against them is loaded by the test suite on every run. Adds pyproject.toml so an installed plugin can announce itself.
|
|
38
|
+
0.10.1 -> the GPU per-pixel path honours a fit window, so dropping a reflection peak out of the fit no longer costs the GPU. Two fitting bugs found and fixed by Zhen Yuan: one-component free-tau gave different answers depending on whether a backend was selected, because the CPU grid-scanned it and the backend used the SciPy solver; and per-pixel distribution fits ignored the fit window that the summed fit honoured, so an excluded artefact stayed in every per-pixel map. Add-ons can now be installed into the compiled app as a wheel, and the GUI explains the fit settings in place.
|
|
39
|
+
0.12.0 -> on PyPI, so pip install flimkit gets the analysis core without the desktop, with gui, torch, mlx, notebook, segmentation and test as extras. Plugins now load when a format is looked up, so a plugin-registered format works from a script or a CLI and not only from the GUI. Per-pixel fitting on the GPU works in pixel blocks, so a field larger than a gigabyte reaches the GPU instead of silently falling back to the CPU; a 1024 square field fits in 7.8s against 25.2s on the CPU. The per-pixel tau grid goes from 200 points to 1600, which cuts the error against known-truth synthetic data from 0.021 ns to 0.008 ns, so per-pixel lifetimes will differ slightly from earlier versions. Cancelling a tile fit is honoured through canvas assembly and saving rather than only during the fitting. Leica .xlef projects are read for tile positions.
|
|
40
|
+
1.0.0 -> release version with all core features implemented and tested
|
|
41
|
+
1.5.0 (or 2.0) -> validation of fitting results with known fluorophores.'''
|
flimkit/cli.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def main(fast=False, cli=False, check_updates=False, no_plugins=False, plugin_path=None):
|
|
6
|
+
if no_plugins:
|
|
7
|
+
os.environ['FLIMKIT_NO_PLUGINS'] = '1'
|
|
8
|
+
if plugin_path:
|
|
9
|
+
existing = os.environ.get('FLIMKIT_PLUGIN_PATH', '')
|
|
10
|
+
os.environ['FLIMKIT_PLUGIN_PATH'] = (
|
|
11
|
+
existing + os.pathsep + os.pathsep.join(plugin_path) if existing
|
|
12
|
+
else os.pathsep.join(plugin_path))
|
|
13
|
+
if check_updates:
|
|
14
|
+
from flimkit.utils.update_check import (
|
|
15
|
+
check_installation_freshness,
|
|
16
|
+
format_update_report,
|
|
17
|
+
)
|
|
18
|
+
report = format_update_report(
|
|
19
|
+
check_installation_freshness(timeout=3.0, do_fetch=True)
|
|
20
|
+
)
|
|
21
|
+
print(report)
|
|
22
|
+
return
|
|
23
|
+
if not cli:
|
|
24
|
+
from flimkit.UI.gui import launch_gui
|
|
25
|
+
launch_gui()
|
|
26
|
+
return
|
|
27
|
+
from flimkit import plugins
|
|
28
|
+
plugins.ensure_loaded()
|
|
29
|
+
from flimkit.interactive import single_FOV_flim_fit, stitch_and_fit, stitch_tiles, timelapse_flim_fit, zstack_flim_fit
|
|
30
|
+
import inquirer
|
|
31
|
+
from flimkit._version import __version__, roadmap
|
|
32
|
+
from flimkit.utils.fancy import display_banner, flim_fitting_banner, banner_goodbye
|
|
33
|
+
if fast == False:
|
|
34
|
+
display_banner()
|
|
35
|
+
print('Welcome to the FLIM data processing tool!')
|
|
36
|
+
questions = [
|
|
37
|
+
inquirer.List(
|
|
38
|
+
'process_option',
|
|
39
|
+
message='Choose a processing option',
|
|
40
|
+
choices=[
|
|
41
|
+
'FLIM FIT a single FOV',
|
|
42
|
+
'Phasor analysis',
|
|
43
|
+
'Reconstruct a FOV and FLIM FIT',
|
|
44
|
+
'Just stitch multiple tiles together',
|
|
45
|
+
'Timelapse batch fit',
|
|
46
|
+
'Z-stack batch fit',
|
|
47
|
+
'About',
|
|
48
|
+
'Exit'
|
|
49
|
+
]
|
|
50
|
+
)
|
|
51
|
+
]
|
|
52
|
+
answers = inquirer.prompt(questions)
|
|
53
|
+
if answers['process_option'] == 'FLIM FIT a single FOV':
|
|
54
|
+
if fast == False:
|
|
55
|
+
flim_fitting_banner()
|
|
56
|
+
print('FLIM FITting a single FOV...')
|
|
57
|
+
single_FOV_flim_fit(interactive=True)
|
|
58
|
+
elif answers['process_option'] == 'Phasor analysis':
|
|
59
|
+
from flimkit.phasor_launcher import phasor_inquire
|
|
60
|
+
phasor_inquire()
|
|
61
|
+
elif answers['process_option'] == 'Reconstruct a FOV and FLIM FIT':
|
|
62
|
+
print('Reconstructing a FOV and FLIM FITting...')
|
|
63
|
+
stitch_and_fit(interactive=True)
|
|
64
|
+
elif answers['process_option'] == 'Just stitch multiple tiles together':
|
|
65
|
+
print('Stitching multiple tiles together...')
|
|
66
|
+
stitch_tiles(interactive=True)
|
|
67
|
+
elif answers['process_option'] == 'Timelapse batch fit':
|
|
68
|
+
print('Timelapse batch FLIM fitting...')
|
|
69
|
+
timelapse_flim_fit(interactive=True)
|
|
70
|
+
elif answers['process_option'] == 'Z-stack batch fit':
|
|
71
|
+
print('Z-stack batch FLIM fitting...')
|
|
72
|
+
zstack_flim_fit(interactive=True)
|
|
73
|
+
elif answers['process_option'] == 'About':
|
|
74
|
+
print('Current version: ' + __version__)
|
|
75
|
+
try:
|
|
76
|
+
from flimkit.utils.update_check import (
|
|
77
|
+
check_installation_freshness,
|
|
78
|
+
format_update_report,
|
|
79
|
+
)
|
|
80
|
+
print()
|
|
81
|
+
print(format_update_report(
|
|
82
|
+
check_installation_freshness(timeout=2.5, do_fetch=False)
|
|
83
|
+
))
|
|
84
|
+
except Exception as exc:
|
|
85
|
+
print(f'Update check unavailable: {exc}')
|
|
86
|
+
print(roadmap)
|
|
87
|
+
return
|
|
88
|
+
else:
|
|
89
|
+
banner_goodbye()
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
def run(argv=None):
|
|
93
|
+
import multiprocessing
|
|
94
|
+
multiprocessing.freeze_support()
|
|
95
|
+
parser = argparse.ArgumentParser(description='FLIMKit — FLIM data processing toolkit')
|
|
96
|
+
parser.add_argument('--cli', action='store_true', help='=Run in CLI mode')
|
|
97
|
+
parser.add_argument('--fast', action='store_true', help='Skip banner display')
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
'--check-updates',
|
|
100
|
+
action='store_true',
|
|
101
|
+
help='Check whether git checkout and local version are up to date, then exit',
|
|
102
|
+
)
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
'--no-plugins',
|
|
105
|
+
action='store_true',
|
|
106
|
+
help='Skip every plugin, built in and user, for a reproducible baseline',
|
|
107
|
+
)
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
'--plugins',
|
|
110
|
+
action='append',
|
|
111
|
+
metavar='DIR',
|
|
112
|
+
help='Extra folder to load plugins from, repeatable',
|
|
113
|
+
)
|
|
114
|
+
args = parser.parse_args(argv)
|
|
115
|
+
main(fast=args.fast, cli=args.cli, check_updates=args.check_updates,
|
|
116
|
+
no_plugins=args.no_plugins, plugin_path=args.plugins)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == '__main__':
|
|
120
|
+
run()
|
flimkit/configs.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from matplotlib.colors import LinearSegmentedColormap
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
FLIM_CMAP = LinearSegmentedColormap.from_list(
|
|
6
|
+
'flim', ['#000080','#0000ff','#00ffff','#00ff00','#ffff00','#ff0000']
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
MIN_PHOTONS_PERPIX = 10
|
|
10
|
+
|
|
11
|
+
# Intensity threshold for masking low-signal pixels before fitting.
|
|
12
|
+
# Set to None (disabled) or an integer photon count.
|
|
13
|
+
# Use --intensity-threshold on the CLI, or 'interactive' to pick visually.
|
|
14
|
+
INTENSITY_THRESHOLD = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# General fitting settings:
|
|
18
|
+
Tau_min = 0.145
|
|
19
|
+
Tau_max = 45.0
|
|
20
|
+
|
|
21
|
+
# Set default fitting mode. Options are "summed", "perPixel", and "both". Override with --mode when running the code.
|
|
22
|
+
D_mode = 'both'
|
|
23
|
+
|
|
24
|
+
# Default number of exponentials to fit. Options are 1, 2, or 3. Override with --nexp when running the code. 4+ exponentials are not supported as the fitting becomes unstable and unreliable.
|
|
25
|
+
n_exp = 3
|
|
26
|
+
|
|
27
|
+
# Default binning factor for per-pixel fitting. Set to 1 for no binning. Override with --binning when running the code.
|
|
28
|
+
binning_factor = 1
|
|
29
|
+
# Default optimizer for per-pixel fitting. Options are "lm_multistart" and "de".
|
|
30
|
+
Optimizer = 'de'
|
|
31
|
+
|
|
32
|
+
# Levenberg-Marquardt settins:
|
|
33
|
+
lm_restarts = 8
|
|
34
|
+
|
|
35
|
+
# Default settings for DE optimizer:
|
|
36
|
+
de_population = 30
|
|
37
|
+
de_maxiter = 5000
|
|
38
|
+
n_workers = -1
|
|
39
|
+
|
|
40
|
+
# IRF settings:
|
|
41
|
+
IRF_FWHM = None
|
|
42
|
+
# ns window (centred on the decay peak) the parametric IRF is fit over. Keep it
|
|
43
|
+
# tight: a wide window pulls in the fluorescence falling edge, so the fit traces
|
|
44
|
+
# the decay instead of the instrument pulse and yields an over-broad IRF.
|
|
45
|
+
IRF_FIT_WIDTH = 0.5
|
|
46
|
+
IRF_BINS = 21
|
|
47
|
+
Estimate_IRF = 'none'
|
|
48
|
+
|
|
49
|
+
# Machine IRF defaults (spreadsheet-free workflow)
|
|
50
|
+
|
|
51
|
+
# True when launched as a PyInstaller-compiled executable.
|
|
52
|
+
_is_frozen = getattr(sys, 'frozen', False)
|
|
53
|
+
|
|
54
|
+
# Bundled read-only resources - always accessible via __file__ (even when frozen,
|
|
55
|
+
# PyInstaller extracts them to a temp dir that __file__ still points into).
|
|
56
|
+
_BUNDLED_MACHINE_IRF_DIR = Path(__file__).resolve().parent / 'machine_irf'
|
|
57
|
+
|
|
58
|
+
# Writable location for user-generated IRF files.
|
|
59
|
+
# When frozen the package dir is inside a read-only bundle, so saves go to
|
|
60
|
+
# ~/.flimkit/machine_irf/ instead. When running from source the behaviour is
|
|
61
|
+
# unchanged (saves alongside the package).
|
|
62
|
+
USER_MACHINE_IRF_DIR = Path.home() / '.flimkit' / 'machine_irf'
|
|
63
|
+
MACHINE_IRF_DIR = USER_MACHINE_IRF_DIR if _is_frozen else _BUNDLED_MACHINE_IRF_DIR
|
|
64
|
+
|
|
65
|
+
# Default IRF path used when no explicit path is given.
|
|
66
|
+
# When frozen: prefer a user-saved copy (if it exists), else fall back to the
|
|
67
|
+
# bundled factory default so a fresh install still works out of the box.
|
|
68
|
+
_user_default = USER_MACHINE_IRF_DIR / 'machine_irf_default.npy'
|
|
69
|
+
MACHINE_IRF_DEFAULT_PATH = (
|
|
70
|
+
(_user_default if _user_default.exists() else _BUNDLED_MACHINE_IRF_DIR / 'machine_irf_default.npy')
|
|
71
|
+
if _is_frozen
|
|
72
|
+
else _BUNDLED_MACHINE_IRF_DIR / 'machine_irf_default.npy'
|
|
73
|
+
)
|
|
74
|
+
MACHINE_IRF_ALIGN_ANCHOR = 'peak'
|
|
75
|
+
MACHINE_IRF_REDUCER = 'median'
|
|
76
|
+
MACHINE_IRF_FIT_STRATEGY = 'fixed'
|
|
77
|
+
MACHINE_IRF_FIT_BG = True
|
|
78
|
+
MACHINE_IRF_FIT_SIGMA = False
|
|
79
|
+
MACHINE_IRF_FIT_TAIL = False
|
|
80
|
+
MACHINE_IRF_SIGMA_MAX_FULL = 3.0
|
|
81
|
+
MACHINE_IRF_SIGMA_MAX_HALF = 0.5
|
|
82
|
+
MACHINE_IRF_DE_POPULATION = 30
|
|
83
|
+
MACHINE_IRF_DE_MAXITER = 5000
|
|
84
|
+
|
|
85
|
+
# Cost function for summed fit. Options are "poisson" (recommended) and "chi2" (legacy).
|
|
86
|
+
# "poisson" uses Poisson deviance (C-statistic) on raw counts - statistically correct.
|
|
87
|
+
# "chi2" normalises by peak and uses Neyman weights - underweights the tail.
|
|
88
|
+
Cost_function = 'poisson'
|
|
89
|
+
|
|
90
|
+
# Display range for exported tau images (FLIM microscope clipping).
|
|
91
|
+
# Out-of-range pixels are clipped to the nearest boundary, matching FLIM microscope software behaviour.
|
|
92
|
+
# Set to None to keep the full fitted range (no clipping).
|
|
93
|
+
TAU_DISPLAY_MIN = None
|
|
94
|
+
TAU_DISPLAY_MAX = None
|
|
95
|
+
|
|
96
|
+
# Display range for exported intensity images (same clipping behaviour).
|
|
97
|
+
# Set to None to keep the full range.
|
|
98
|
+
INTENSITY_DISPLAY_MIN = None
|
|
99
|
+
INTENSITY_DISPLAY_MAX = None
|
|
100
|
+
|
|
101
|
+
# Phasor-domain spatial filtering applied after calibration.
|
|
102
|
+
# Set to None to disable. Options: None, 'gaussian', 'median', 'wavelet'.
|
|
103
|
+
PHASOR_FILTER = None
|
|
104
|
+
PHASOR_FILTER_SIGMA = 1.0
|
|
105
|
+
PHASOR_FILTER_SIZE = 3
|
|
106
|
+
PHASOR_FILTER_WAVELET = 'db4'
|
|
107
|
+
PHASOR_FILTER_LEVEL = 1
|
|
108
|
+
|
|
109
|
+
# Other specific settings:
|
|
110
|
+
channels = None
|
|
111
|
+
OUT_NAME = 'flim_out'
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
config_message = f"""Default settings:
|
|
115
|
+
Intensity threshold: {INTENSITY_THRESHOLD} photons (None = disabled)
|
|
116
|
+
Tau_min: {Tau_min} ns
|
|
117
|
+
Tau_max: {Tau_max} ns
|
|
118
|
+
Tau display min: {TAU_DISPLAY_MIN} ns (None = no clip)
|
|
119
|
+
Tau display max: {TAU_DISPLAY_MAX} ns (None = no clip)
|
|
120
|
+
Intensity display min: {INTENSITY_DISPLAY_MIN} (None = no clip)
|
|
121
|
+
Intensity display max: {INTENSITY_DISPLAY_MAX} (None = no clip)
|
|
122
|
+
Fitting mode: {D_mode}
|
|
123
|
+
Number of exponentials: {n_exp}
|
|
124
|
+
Cost function: {Cost_function}
|
|
125
|
+
Optimizer: {Optimizer}
|
|
126
|
+
Levenberg-Marquardt restarts: {lm_restarts}
|
|
127
|
+
Differential Evolution population size: {de_population}
|
|
128
|
+
Differential Evolution max iterations: {de_maxiter}
|
|
129
|
+
Number of workers for DE optimization: {n_workers}
|
|
130
|
+
IRF FWHM: {IRF_FWHM} ns
|
|
131
|
+
IRF fit width: {IRF_FIT_WIDTH} ns
|
|
132
|
+
IRF bins: {IRF_BINS}
|
|
133
|
+
IRF estimation method: {Estimate_IRF}
|
|
134
|
+
Machine IRF default path: {MACHINE_IRF_DEFAULT_PATH}
|
|
135
|
+
Machine IRF align anchor: {MACHINE_IRF_ALIGN_ANCHOR}
|
|
136
|
+
Machine IRF reducer: {MACHINE_IRF_REDUCER}
|
|
137
|
+
Machine IRF fit strategy: {MACHINE_IRF_FIT_STRATEGY}
|
|
138
|
+
Machine IRF fit bg/sigma/tail: {MACHINE_IRF_FIT_BG}/{MACHINE_IRF_FIT_SIGMA}/{MACHINE_IRF_FIT_TAIL}
|
|
139
|
+
Machine IRF DE population/maxiter: {MACHINE_IRF_DE_POPULATION}/{MACHINE_IRF_DE_MAXITER}
|
|
140
|
+
Channels to fit: {channels}
|
|
141
|
+
Output directory: {OUT_NAME}
|
|
142
|
+
Change any of these defaults by passing the corresponding argument when running the code.
|
|
143
|
+
Run `python -m flim_fitter --help` for more details on available arguments and their usage.
|
|
144
|
+
Change defaults in configs.py or override with command line arguments as needed for different datasets and systems.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
if __name__ == '__main__':
|
|
148
|
+
print(config_message)
|
flimkit/dialogs.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
_provider = None
|
|
2
|
+
|
|
3
|
+
def set_dialog_provider(fn):
|
|
4
|
+
global _provider
|
|
5
|
+
_provider = fn
|
|
6
|
+
|
|
7
|
+
def get_dialog_provider():
|
|
8
|
+
return _provider
|
|
9
|
+
|
|
10
|
+
def _tk_save_path(title, default_name, defaultextension, filetypes):
|
|
11
|
+
import tkinter as tk
|
|
12
|
+
from tkinter import filedialog
|
|
13
|
+
existing = tk._default_root
|
|
14
|
+
if existing is not None:
|
|
15
|
+
parent = existing
|
|
16
|
+
need_destroy = False
|
|
17
|
+
else:
|
|
18
|
+
parent = tk.Tk()
|
|
19
|
+
parent.withdraw()
|
|
20
|
+
need_destroy = True
|
|
21
|
+
parent.attributes('-topmost', True)
|
|
22
|
+
parent.update()
|
|
23
|
+
path = filedialog.asksaveasfilename(
|
|
24
|
+
parent=parent,
|
|
25
|
+
title=title,
|
|
26
|
+
defaultextension=defaultextension,
|
|
27
|
+
initialfile=default_name,
|
|
28
|
+
filetypes=filetypes)
|
|
29
|
+
if need_destroy:
|
|
30
|
+
parent.destroy()
|
|
31
|
+
return path or None
|
|
32
|
+
|
|
33
|
+
def ask_save_path(title, default_name, defaultextension='', filetypes=None):
|
|
34
|
+
filetypes = filetypes or [('All files', '*')]
|
|
35
|
+
if _provider is not None:
|
|
36
|
+
try:
|
|
37
|
+
return _provider(title=title, default_name=default_name,
|
|
38
|
+
defaultextension=defaultextension,
|
|
39
|
+
filetypes=filetypes)
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
try:
|
|
43
|
+
return _tk_save_path(title, default_name, defaultextension, filetypes)
|
|
44
|
+
except Exception:
|
|
45
|
+
path = input(f'Save path [{default_name}]: ').strip().strip('"')
|
|
46
|
+
return path or default_name
|
|
File without changes
|