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.
Files changed (104) hide show
  1. flimkit/FLIM/__init__.py +0 -0
  2. flimkit/FLIM/assemble.py +254 -0
  3. flimkit/FLIM/batch.py +681 -0
  4. flimkit/FLIM/bg_tools.py +51 -0
  5. flimkit/FLIM/fit_tools.py +244 -0
  6. flimkit/FLIM/fitters.py +1471 -0
  7. flimkit/FLIM/irf_tools.py +617 -0
  8. flimkit/FLIM/models.py +391 -0
  9. flimkit/GPU/__init__.py +85 -0
  10. flimkit/GPU/_base.py +391 -0
  11. flimkit/GPU/cuda.py +10 -0
  12. flimkit/GPU/mlx_backend.py +381 -0
  13. flimkit/GPU/mps.py +10 -0
  14. flimkit/GPU/rocm.py +10 -0
  15. flimkit/GPU/torch_backend.py +385 -0
  16. flimkit/UI/app_state.py +10 -0
  17. flimkit/UI/controller.py +139 -0
  18. flimkit/UI/expert_settings.py +248 -0
  19. flimkit/UI/fit_help.py +206 -0
  20. flimkit/UI/fov_preview.py +1085 -0
  21. flimkit/UI/gui.py +3919 -0
  22. flimkit/UI/icon.icns +0 -0
  23. flimkit/UI/icon.ico +0 -0
  24. flimkit/UI/icon.png +0 -0
  25. flimkit/UI/irf_widget.py +103 -0
  26. flimkit/UI/mode_controller.py +118 -0
  27. flimkit/UI/modes/__init__.py +0 -0
  28. flimkit/UI/modes/base.py +3 -0
  29. flimkit/UI/modes/batch_mode.py +312 -0
  30. flimkit/UI/modes/fov_mode.py +164 -0
  31. flimkit/UI/modes/irf_mode.py +80 -0
  32. flimkit/UI/modes/phasor_mode.py +131 -0
  33. flimkit/UI/modes/stitch_mode.py +254 -0
  34. flimkit/UI/phasor_panel.py +1087 -0
  35. flimkit/UI/progress_window.py +113 -0
  36. flimkit/UI/project_panel.py +262 -0
  37. flimkit/UI/results_panel.py +332 -0
  38. flimkit/UI/roi_tools.py +794 -0
  39. flimkit/UI/utils.py +217 -0
  40. flimkit/__init__.py +0 -0
  41. flimkit/_version.py +41 -0
  42. flimkit/cli.py +120 -0
  43. flimkit/configs.py +148 -0
  44. flimkit/dialogs.py +46 -0
  45. flimkit/formats/BH/__init__.py +0 -0
  46. flimkit/formats/BH/reader.py +296 -0
  47. flimkit/formats/BH/writer.py +86 -0
  48. flimkit/formats/ISS/__init__.py +0 -0
  49. flimkit/formats/ISS/fdflim.py +86 -0
  50. flimkit/formats/ISS/image.py +114 -0
  51. flimkit/formats/ISS/reader.py +223 -0
  52. flimkit/formats/PS/__init__.py +0 -0
  53. flimkit/formats/PS/reader.py +202 -0
  54. flimkit/formats/PTU/__init__.py +0 -0
  55. flimkit/formats/PTU/decode.py +27 -0
  56. flimkit/formats/PTU/phu.py +85 -0
  57. flimkit/formats/PTU/reader.py +235 -0
  58. flimkit/formats/PTU/series.py +258 -0
  59. flimkit/formats/PTU/stitch.py +1182 -0
  60. flimkit/formats/PTU/tools.py +94 -0
  61. flimkit/formats/__init__.py +2 -0
  62. flimkit/formats/flim_file.py +232 -0
  63. flimkit/formats/phasor.py +132 -0
  64. flimkit/formats/signal.py +170 -0
  65. flimkit/image/tools.py +124 -0
  66. flimkit/interactive.py +1857 -0
  67. flimkit/mpl_backend.py +22 -0
  68. flimkit/phasor/__init__.py +40 -0
  69. flimkit/phasor/filters.py +127 -0
  70. flimkit/phasor/fret.py +654 -0
  71. flimkit/phasor/interactive.py +556 -0
  72. flimkit/phasor/peaks.py +186 -0
  73. flimkit/phasor/signal.py +90 -0
  74. flimkit/phasor_launcher.py +314 -0
  75. flimkit/plugins/__init__.py +137 -0
  76. flimkit/plugins/bindings.py +116 -0
  77. flimkit/plugins/builtin/__init__.py +3 -0
  78. flimkit/plugins/builtin/core_tools.py +28 -0
  79. flimkit/plugins/loader.py +371 -0
  80. flimkit/plugins/registry.py +406 -0
  81. flimkit/project.py +197 -0
  82. flimkit/synth.py +145 -0
  83. flimkit/utils/__init__.py +0 -0
  84. flimkit/utils/batch_fit.py +301 -0
  85. flimkit/utils/config_manager.py +119 -0
  86. flimkit/utils/config_snapshot.py +30 -0
  87. flimkit/utils/crash_handler.py +183 -0
  88. flimkit/utils/display.py +197 -0
  89. flimkit/utils/enhanced_outputs.py +345 -0
  90. flimkit/utils/fancy.py +103 -0
  91. flimkit/utils/lifetime_image.py +243 -0
  92. flimkit/utils/misc.py +111 -0
  93. flimkit/utils/plotting.py +190 -0
  94. flimkit/utils/roi.py +370 -0
  95. flimkit/utils/session.py +51 -0
  96. flimkit/utils/update_check.py +198 -0
  97. flimkit/utils/xlsx_tools.py +97 -0
  98. flimkit/utils/xml_utils.py +219 -0
  99. flimkit-0.12.0.dist-info/METADATA +356 -0
  100. flimkit-0.12.0.dist-info/RECORD +104 -0
  101. flimkit-0.12.0.dist-info/WHEEL +5 -0
  102. flimkit-0.12.0.dist-info/entry_points.txt +2 -0
  103. flimkit-0.12.0.dist-info/licenses/LICENSE.md +11 -0
  104. flimkit-0.12.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,332 @@
1
+ from __future__ import annotations
2
+ import os
3
+ import re
4
+ import json
5
+ import time
6
+ import inspect
7
+ from pathlib import Path
8
+ from typing import Optional
9
+ import tkinter as tk
10
+ from tkinter import ttk, filedialog, messagebox, scrolledtext, simpledialog
11
+ import numpy as np
12
+ import matplotlib
13
+ import matplotlib.image as mpimg
14
+ from matplotlib.figure import Figure
15
+ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
16
+ from flimkit.utils import display
17
+
18
+ class ResultsPanel:
19
+ def __init__(self, parent, root=None):
20
+ self.parent = parent
21
+ self.root = root
22
+ self.frame = ttk.Frame(parent)
23
+ self.frame.columnconfigure(0, weight=1)
24
+ self.frame.rowconfigure(0, weight=1)
25
+ self._nb = ttk.Notebook(self.frame)
26
+ self._nb.grid(row=0, column=0, sticky='nsew')
27
+ self._build_progress()
28
+ self._build_summary()
29
+ self._build_images()
30
+ self._imgs = []
31
+ self._folder = None
32
+ self._img_i = 0
33
+ self._fit_result = None
34
+ self._output_dir = None
35
+ self._current_npz_path = None
36
+ self._scan_name = None
37
+ self._export_callback = None
38
+ self._load_callback = None
39
+ self._save_npz_callback = None
40
+ self._status = tk.StringVar(value='Ready.')
41
+ ttk.Label(self.frame, textvariable=self._status, foreground='grey').grid(
42
+ row=1, column=0, sticky='w', padx=4, pady=(2, 4))
43
+
44
+ def _build_progress(self):
45
+ f = ttk.Frame(self._nb, padding=4)
46
+ self._nb.add(f, text=' Progress ')
47
+ f.columnconfigure(0, weight=1)
48
+ f.rowconfigure(0, weight=1)
49
+ self.log = scrolledtext.ScrolledText(
50
+ f, state='disabled', wrap='word',
51
+ font=('Courier', 9), background='#1e1e1e', foreground='#d4d4d4')
52
+ self.log.grid(row=0, column=0, sticky='nsew')
53
+ btn_bar = ttk.Frame(f)
54
+ btn_bar.grid(row=1, column=0, sticky='ew', pady=(4, 0))
55
+ ttk.Button(btn_bar, text='Save log...', command=self._save_log).pack(side='left', padx=4)
56
+ ttk.Button(btn_bar, text='Clear log', command=self._clear_log).pack(side='right', padx=4)
57
+
58
+ def _clear_log(self):
59
+ self.log.configure(state='normal')
60
+ self.log.delete('1.0', tk.END)
61
+ self.log.configure(state='disabled')
62
+
63
+ def _save_log(self):
64
+ text = self.log.get('1.0', tk.END)
65
+ if not text.strip():
66
+ messagebox.showinfo('Nothing to save', 'The log is empty.')
67
+ return
68
+ path = filedialog.asksaveasfilename(
69
+ title='Save log as...',
70
+ initialfile=f"{self._scan_name}_log.txt" if self._scan_name else '',
71
+ defaultextension='.txt',
72
+ filetypes=[('Text files', '*.txt'), ('All', '*.*')])
73
+ if path:
74
+ Path(path).write_text(text, encoding='utf-8')
75
+ self._status.set(f"Log saved → {Path(path).name}")
76
+
77
+ def _on_export_clicked(self):
78
+ try:
79
+ print(f"[Export Button] Clicked - callback={self._export_callback is not None}, fit_result={self._fit_result is not None}, output_dir={self._output_dir}")
80
+ if self._export_callback and self._fit_result and self._output_dir:
81
+ print(f"[Export Button] Calling callback...")
82
+ self._export_callback(self._fit_result, self._output_dir)
83
+ else:
84
+ print(f"[Export Button] Missing: callback={self._export_callback} fit_result={self._fit_result is not None} output_dir={self._output_dir}")
85
+ except Exception as e:
86
+ print(f"[Export Button Error] {e}")
87
+ import traceback
88
+ traceback.print_exc()
89
+
90
+ def set_fit_result(self, fit_result: dict, output_dir: str, npz_path: str = None, scan_name: str = None):
91
+ self._fit_result = fit_result
92
+ self._output_dir = output_dir
93
+ if npz_path:
94
+ self._current_npz_path = npz_path
95
+ if scan_name:
96
+ self._scan_name = scan_name
97
+ has_images = any(isinstance(v, np.ndarray) for v in (fit_result or {}).values())
98
+ self._export_btn.configure(state='normal' if has_images else 'disabled')
99
+
100
+ def set_export_callback(self, callback):
101
+ self._export_callback = callback
102
+
103
+ def set_load_callback(self, callback):
104
+ self._load_callback = callback
105
+
106
+ def set_save_npz_callback(self, callback):
107
+ self._save_npz_callback = callback
108
+
109
+ def _on_save_npz_clicked(self):
110
+ try:
111
+ if self._save_npz_callback and self._output_dir:
112
+ self._save_npz_callback(self._output_dir)
113
+ except Exception as e:
114
+ print(f"[Save NPZ Error] {e}")
115
+ import traceback
116
+ traceback.print_exc()
117
+
118
+ def _export_summed_csv(self):
119
+ try:
120
+ import csv
121
+ from pathlib import Path
122
+ init_name = f"{self._scan_name}_summed_fit.csv" if self._scan_name else None
123
+ csv_file = filedialog.asksaveasfilename(
124
+ title='Export Summed Fit Data',
125
+ initialfile=init_name,
126
+ defaultextension='.csv',
127
+ filetypes=[('CSV files', '*.csv'), ('All files', '*.*')],
128
+ initialdir=self._output_dir)
129
+ if not csv_file:
130
+ return
131
+ rows = []
132
+ for item in self._tv.get_children():
133
+ values = self._tv.item(item)['values']
134
+ rows.append(values)
135
+ if not rows:
136
+ messagebox.showwarning('No Data', 'No summary data to export.')
137
+ return
138
+ with open(csv_file, 'w', newline='', encoding='utf-8') as f:
139
+ writer = csv.writer(f)
140
+ writer.writerow(['Parameter', 'Value', 'Unit'])
141
+ writer.writerows(rows)
142
+ messagebox.showinfo('Export Success', f"Summed fit data exported to:\n{Path(csv_file).name}")
143
+ self._status.set(f"Exported → {Path(csv_file).name}")
144
+ print(f"[Export] Summed fit CSV: {csv_file}")
145
+ except Exception as e:
146
+ messagebox.showerror('Export Error', f"Failed to export CSV:\n{e}")
147
+ import traceback
148
+ traceback.print_exc()
149
+
150
+ def _load_fitted_data(self):
151
+ npz_file = filedialog.askopenfilename(
152
+ title='Load Fitted Data',
153
+ filetypes=[('NumPy Archives', '*.npz'), ('All', '*.*')],
154
+ defaultextension='.npz')
155
+ if not npz_file:
156
+ return
157
+ if self._load_callback:
158
+ self._load_callback(npz_file)
159
+
160
+ def _build_summary(self):
161
+ f = ttk.Frame(self._nb, padding=4)
162
+ self._nb.add(f, text=' Fit Summary ')
163
+ f.columnconfigure(0, weight=1)
164
+ f.rowconfigure(0, weight=1)
165
+ cols = ('Parameter', 'Value', 'Unit')
166
+ tv = ttk.Treeview(f, columns=cols, show='headings')
167
+ tv.heading('Parameter', text='Parameter', anchor='w')
168
+ tv.heading('Value', text='Value', anchor='e')
169
+ tv.heading('Unit', text='Unit', anchor='w')
170
+ tv.column('Parameter', width=300, anchor='w', stretch=True)
171
+ tv.column('Value', width=110, anchor='e', stretch=False)
172
+ tv.column('Unit', width=70, anchor='w', stretch=False)
173
+ sb = ttk.Scrollbar(f, orient='vertical', command=tv.yview)
174
+ tv.configure(yscrollcommand=sb.set)
175
+ tv.grid(row=0, column=0, sticky='nsew')
176
+ sb.grid(row=0, column=1, sticky='ns')
177
+ tv.tag_configure('odd', background='#f5f7fa', foreground='#000000')
178
+ tv.tag_configure('even', background='#ffffff', foreground='#000000')
179
+ tv.tag_configure('warn', foreground='#c0550a', background='#fff8f0')
180
+ self._tv = tv
181
+ btn_bar = ttk.Frame(f)
182
+ btn_bar.grid(row=1, column=0, columnspan=2, sticky='ew', pady=(4, 0))
183
+ self._export_btn = ttk.Button(btn_bar, text='Export Images...',
184
+ command=self._on_export_clicked, state='disabled')
185
+ self._export_btn.pack(side='left', padx=4)
186
+
187
+ def _build_images(self):
188
+ f = ttk.Frame(self._nb, padding=4)
189
+ self._nb.add(f, text=' Images ')
190
+ f.columnconfigure(0, weight=1)
191
+ f.rowconfigure(0, weight=1)
192
+ fig = Figure(figsize=(5, 5), facecolor='#2b2b2b')
193
+ self._ax = fig.add_subplot(111)
194
+ self._ax.set_facecolor('#2b2b2b')
195
+ self._ax.axis('off')
196
+ self._canvas_mpl = FigureCanvasTkAgg(fig, master=f)
197
+ self._canvas_mpl.get_tk_widget().grid(row=0, column=0, sticky='nsew')
198
+ self._img_lbl = tk.StringVar(value='No images loaded')
199
+ ttk.Label(f, textvariable=self._img_lbl, foreground='grey').grid(
200
+ row=1, column=0, sticky='w', padx=4, pady=(2, 0))
201
+ self._img_slider_updating = False
202
+ self._img_slider = ttk.Scale(f, from_=0, to=0, orient='horizontal',
203
+ command=self._on_img_slider)
204
+ self._img_slider.grid(row=2, column=0, sticky='ew', padx=4, pady=(2, 0))
205
+ nav = ttk.Frame(f)
206
+ nav.grid(row=3, column=0, sticky='ew', pady=(4, 0))
207
+ ttk.Button(nav, text='◀ Prev', command=self._img_prev).pack(side='left', padx=4)
208
+ ttk.Button(nav, text='Next ▶', command=self._img_next).pack(side='left', padx=4)
209
+ ttk.Button(nav, text='Open folder', command=self._open_folder).pack(side='right', padx=4)
210
+ ttk.Button(nav, text='Save all...', command=self._save_all_imgs).pack(side='right', padx=4)
211
+ ttk.Button(nav, text='Save image...', command=self._save_img).pack(side='right', padx=4)
212
+
213
+ def populate_summary(self, rows: list):
214
+ for item in self._tv.get_children():
215
+ self._tv.delete(item)
216
+ self._tv.update()
217
+ for i, (param, val, unit) in enumerate(rows):
218
+ tag = 'warn' if param.startswith('⚠') else ('odd' if i % 2 else 'even')
219
+ self._tv.insert('', tk.END, values=(param, val, unit), tags=(tag,))
220
+ self._tv.update_idletasks()
221
+ if rows:
222
+ self._nb.select(1)
223
+ self._nb.update_idletasks()
224
+
225
+ def set_status(self, msg: str):
226
+ self._status.set(msg)
227
+
228
+ def grid(self, **kw):
229
+ self.frame.grid(**kw)
230
+
231
+ def load_images(self, folder: Optional[str]):
232
+ self._imgs = []
233
+ if folder and Path(folder).is_dir():
234
+ self._folder = folder
235
+ for pat in ('*.png', '*.tif', '*.tiff'):
236
+ self._imgs += sorted(Path(folder).glob(pat))
237
+ self._img_i = 0
238
+ self._img_slider.configure(to=max(len(self._imgs) - 1, 0))
239
+ self._sync_img_slider()
240
+ self._draw_img()
241
+ if self._imgs:
242
+ self._nb.select(2)
243
+
244
+ def _sync_img_slider(self):
245
+ self._img_slider_updating = True
246
+ try:
247
+ self._img_slider.set(self._img_i)
248
+ finally:
249
+ self._img_slider_updating = False
250
+
251
+ def _on_img_slider(self, value):
252
+ if self._img_slider_updating or not getattr(self, '_imgs', None):
253
+ return
254
+ i = max(0, min(int(round(float(value))), len(self._imgs) - 1))
255
+ if i != self._img_i:
256
+ self._img_i = i
257
+ self._draw_img()
258
+
259
+ def _draw_img(self):
260
+ self._ax.cla()
261
+ self._ax.set_facecolor('#2b2b2b')
262
+ self._ax.axis('off')
263
+ if not self._imgs:
264
+ self._img_lbl.set('No images found')
265
+ self._ax.text(0.5, 0.5, 'No images found',
266
+ ha='center', va='center', color='grey', fontsize=11,
267
+ transform=self._ax.transAxes)
268
+ else:
269
+ path = self._imgs[self._img_i]
270
+ self._img_lbl.set(f"{path.name} ({self._img_i + 1}/{len(self._imgs)})")
271
+ try:
272
+ img = mpimg.imread(str(path))
273
+ self._ax.imshow(img, aspect='equal')
274
+ except Exception as e:
275
+ self._ax.text(0.5, 0.5, f"Cannot load image:\n{e}",
276
+ ha='center', va='center', color='red',
277
+ fontsize=9, transform=self._ax.transAxes)
278
+ self._canvas_mpl.draw_idle()
279
+
280
+ def _img_prev(self):
281
+ if self._imgs:
282
+ self._img_i = (self._img_i - 1) % len(self._imgs)
283
+ self._sync_img_slider()
284
+ self._draw_img()
285
+
286
+ def _img_next(self):
287
+ if self._imgs:
288
+ self._img_i = (self._img_i + 1) % len(self._imgs)
289
+ self._sync_img_slider()
290
+ self._draw_img()
291
+
292
+ def _save_img(self):
293
+ if not self._imgs:
294
+ messagebox.showinfo('No image', 'No image is currently displayed.')
295
+ return
296
+ src = self._imgs[self._img_i]
297
+ path = filedialog.asksaveasfilename(
298
+ title='Save current image as...',
299
+ initialfile=src.name,
300
+ defaultextension=src.suffix,
301
+ filetypes=[
302
+ ('PNG', '*.png'),
303
+ ('TIFF', '*.tif *.tiff'),
304
+ ('All', '*.*'),
305
+ ])
306
+ if path:
307
+ import shutil
308
+ shutil.copy2(str(src), path)
309
+ self._status.set(f"Image saved → {Path(path).name}")
310
+
311
+ def _save_all_imgs(self):
312
+ if not self._imgs:
313
+ messagebox.showinfo('No images', 'No images are available to save.')
314
+ return
315
+ dest = filedialog.askdirectory(title='Save all images to...')
316
+ if not dest:
317
+ return
318
+ import shutil
319
+ dest_path = Path(dest)
320
+ for img in self._imgs:
321
+ shutil.copy2(str(img), str(dest_path / img.name))
322
+ self._status.set(f"{len(self._imgs)} image(s) saved → {dest_path.name}/")
323
+
324
+ def _open_folder(self):
325
+ import subprocess, platform
326
+ if not self._folder or not Path(self._folder).exists():
327
+ messagebox.showinfo('No folder', 'No output folder available yet.')
328
+ return
329
+ s = platform.system()
330
+ if s == 'Darwin': subprocess.Popen(['open', self._folder])
331
+ elif s == 'Windows': subprocess.Popen(['explorer', self._folder])
332
+ else: subprocess.Popen(['xdg-open', self._folder])