flimkit 0.13.2__tar.gz → 0.13.4__tar.gz
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-0.13.2 → flimkit-0.13.4}/PKG-INFO +21 -5
- {flimkit-0.13.2 → flimkit-0.13.4}/README.md +19 -4
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/gui.py +70 -5
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/_version.py +3 -1
- flimkit-0.13.4/flimkit/utils/ome_zarr.py +127 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit.egg-info/PKG-INFO +21 -5
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit.egg-info/SOURCES.txt +1 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit.egg-info/requires.txt +1 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/pyproject.toml +1 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/LICENSE.md +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/FLIM/__init__.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/FLIM/assemble.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/FLIM/batch.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/FLIM/bg_tools.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/FLIM/fit_tools.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/FLIM/fitters.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/FLIM/irf_tools.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/FLIM/models.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/GPU/__init__.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/GPU/_base.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/GPU/cuda.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/GPU/mlx_backend.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/GPU/mps.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/GPU/rocm.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/GPU/torch_backend.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/app_state.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/controller.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/expert_settings.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/fit_help.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/fov_preview.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/icon.icns +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/icon.ico +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/icon.png +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/irf_widget.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/mode_controller.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/modes/__init__.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/modes/base.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/modes/batch_mode.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/modes/fov_mode.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/modes/irf_mode.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/modes/phasor_mode.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/modes/stitch_mode.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/phasor_panel.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/progress_window.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/project_panel.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/results_panel.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/roi_tools.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/utils.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/__init__.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/cli.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/configs.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/dialogs.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/BH/__init__.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/BH/reader.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/BH/writer.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/ISS/__init__.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/ISS/fdflim.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/ISS/image.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/ISS/reader.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/PS/__init__.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/PS/reader.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/PTU/__init__.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/PTU/decode.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/PTU/phu.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/PTU/reader.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/PTU/series.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/PTU/stitch.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/PTU/tools.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/__init__.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/flim_file.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/phasor.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/signal.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/image/tools.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/interactive.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/mpl_backend.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/phasor/__init__.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/phasor/filters.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/phasor/fret.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/phasor/interactive.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/phasor/peaks.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/phasor/signal.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/phasor_launcher.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/plugins/__init__.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/plugins/bindings.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/plugins/builtin/__init__.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/plugins/builtin/core_tools.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/plugins/loader.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/plugins/registry.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/project.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/synth.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/__init__.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/batch_fit.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/config_manager.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/config_snapshot.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/crash_handler.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/display.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/enhanced_outputs.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/fancy.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/lifetime_image.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/misc.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/plotting.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/roi.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/session.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/update_check.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/xlsx_tools.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/xml_utils.py +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit.egg-info/dependency_links.txt +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit.egg-info/entry_points.txt +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/flimkit.egg-info/top_level.txt +0 -0
- {flimkit-0.13.2 → flimkit-0.13.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flimkit
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.4
|
|
4
4
|
Summary: Fluorescence lifetime imaging analysis: reading, fitting, phasors and stitching
|
|
5
5
|
Author-email: Alex Hunt <alexander.hunt@ed.ac.uk>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -38,6 +38,7 @@ Requires-Dist: shapely==2.1.2
|
|
|
38
38
|
Requires-Dist: tifffile
|
|
39
39
|
Requires-Dist: tqdm==4.67.3
|
|
40
40
|
Requires-Dist: xarray==2026.1.0
|
|
41
|
+
Requires-Dist: zarr>=2.18
|
|
41
42
|
Provides-Extra: gui
|
|
42
43
|
Requires-Dist: tkinterdnd2>=0.6.2; extra == "gui"
|
|
43
44
|
Requires-Dist: TKinterModernThemes; extra == "gui"
|
|
@@ -86,9 +87,25 @@ Desktop GUI showing reconvolution fitting with per-pixel lifetime maps, summed d
|
|
|
86
87
|
|
|
87
88
|
## Installation
|
|
88
89
|
|
|
90
|
+
### No Python, no terminal
|
|
91
|
+
|
|
92
|
+
Download the build for your machine from the
|
|
93
|
+
[Releases](https://github.com/FLIMKit/FLIMKit/releases/latest) tab, unzip it,
|
|
94
|
+
and run it. Python is inside it, so there is nothing else to install.
|
|
95
|
+
`FLIMKit-windows.zip`, `FLIMKit-macos.zip` and `FLIMKit-linux.zip` are attached
|
|
96
|
+
to every release.
|
|
97
|
+
|
|
98
|
+
On macOS the app is self-signed, so the first launch needs right-click then
|
|
99
|
+
Open. A double-click will be refused.
|
|
100
|
+
|
|
101
|
+
This route gets the desktop application only. Everything below is for scripting,
|
|
102
|
+
the terminal, or GPU fitting.
|
|
103
|
+
|
|
104
|
+
### From PyPI
|
|
105
|
+
|
|
89
106
|
Python ≥ 3.12 required (3.14 recommended, official builds use 3.14).
|
|
90
107
|
|
|
91
|
-
|
|
108
|
+
For analysis in scripts, notebooks and the terminal:
|
|
92
109
|
|
|
93
110
|
```bash
|
|
94
111
|
pip install flimkit # readers, fitting, phasors, stitching, the CLI
|
|
@@ -114,8 +131,6 @@ For development work (PyInstaller + test dependencies):
|
|
|
114
131
|
python install.py --dev
|
|
115
132
|
```
|
|
116
133
|
|
|
117
|
-
Or download the compiled app from the Releases tab (no Python needed).
|
|
118
|
-
|
|
119
134
|
## Docker / TrueNAS SCALE
|
|
120
135
|
|
|
121
136
|
A pre-built image is available on Docker Hub. It runs the full desktop GUI in a browser via xpra (no installation needed on the client).
|
|
@@ -310,6 +325,7 @@ python run_tests.py integration # integration tests only
|
|
|
310
325
|
|---|---|
|
|
311
326
|
| PNG | Intensity and lifetime map images |
|
|
312
327
|
| OME-TIFF | Lossless export with metadata - opens in Fiji/ImageJ |
|
|
328
|
+
| OME-Zarr | Lossless and compressed, one channel per map (OME-NGFF 0.4) |
|
|
313
329
|
| GeoJSON | ROI geometries and stats - imports directly into QuPath |
|
|
314
330
|
| CSV | Fit summaries and per-ROI statistics |
|
|
315
331
|
| NPZ | Session files for restoring analysis state |
|
|
@@ -346,7 +362,7 @@ Fitted lifetimes from FLIMKit will typically read slightly higher than FLIM micr
|
|
|
346
362
|
|
|
347
363
|
## Acknowledgements
|
|
348
364
|
|
|
349
|
-
FLIMKit is designed, developed, and maintained by Alex Hunt. Anthropic's Claude AI was used as an assistant for parts of the GUI implementation, compiled app builds, code debugging, and Docker packaging; all scientific design, fitting/phasor methods, validation, and the overall architecture are the author's own work.
|
|
365
|
+
FLIMKit is designed, developed, and maintained by Alex Hunt. Anthropic's Claude AI was used as an assistant for parts of the GUI implementation, the add-on system, compiled app builds, code debugging, and Docker packaging; all scientific design, fitting/phasor methods, validation, and the overall architecture are the author's own work.
|
|
350
366
|
|
|
351
367
|
Contributions by Zhen Yuan were developed with assistance from OpenAI's GPT-5.6 Sol, operated through Hermes Agent by Nous Research. This assistance supported implementation, testing, and documentation. Zhen Yuan directed and reviewed the work and remains responsible for the submitted implementations.
|
|
352
368
|
|
|
@@ -25,9 +25,25 @@ Desktop GUI showing reconvolution fitting with per-pixel lifetime maps, summed d
|
|
|
25
25
|
|
|
26
26
|
## Installation
|
|
27
27
|
|
|
28
|
+
### No Python, no terminal
|
|
29
|
+
|
|
30
|
+
Download the build for your machine from the
|
|
31
|
+
[Releases](https://github.com/FLIMKit/FLIMKit/releases/latest) tab, unzip it,
|
|
32
|
+
and run it. Python is inside it, so there is nothing else to install.
|
|
33
|
+
`FLIMKit-windows.zip`, `FLIMKit-macos.zip` and `FLIMKit-linux.zip` are attached
|
|
34
|
+
to every release.
|
|
35
|
+
|
|
36
|
+
On macOS the app is self-signed, so the first launch needs right-click then
|
|
37
|
+
Open. A double-click will be refused.
|
|
38
|
+
|
|
39
|
+
This route gets the desktop application only. Everything below is for scripting,
|
|
40
|
+
the terminal, or GPU fitting.
|
|
41
|
+
|
|
42
|
+
### From PyPI
|
|
43
|
+
|
|
28
44
|
Python ≥ 3.12 required (3.14 recommended, official builds use 3.14).
|
|
29
45
|
|
|
30
|
-
|
|
46
|
+
For analysis in scripts, notebooks and the terminal:
|
|
31
47
|
|
|
32
48
|
```bash
|
|
33
49
|
pip install flimkit # readers, fitting, phasors, stitching, the CLI
|
|
@@ -53,8 +69,6 @@ For development work (PyInstaller + test dependencies):
|
|
|
53
69
|
python install.py --dev
|
|
54
70
|
```
|
|
55
71
|
|
|
56
|
-
Or download the compiled app from the Releases tab (no Python needed).
|
|
57
|
-
|
|
58
72
|
## Docker / TrueNAS SCALE
|
|
59
73
|
|
|
60
74
|
A pre-built image is available on Docker Hub. It runs the full desktop GUI in a browser via xpra (no installation needed on the client).
|
|
@@ -249,6 +263,7 @@ python run_tests.py integration # integration tests only
|
|
|
249
263
|
|---|---|
|
|
250
264
|
| PNG | Intensity and lifetime map images |
|
|
251
265
|
| OME-TIFF | Lossless export with metadata - opens in Fiji/ImageJ |
|
|
266
|
+
| OME-Zarr | Lossless and compressed, one channel per map (OME-NGFF 0.4) |
|
|
252
267
|
| GeoJSON | ROI geometries and stats - imports directly into QuPath |
|
|
253
268
|
| CSV | Fit summaries and per-ROI statistics |
|
|
254
269
|
| NPZ | Session files for restoring analysis state |
|
|
@@ -285,7 +300,7 @@ Fitted lifetimes from FLIMKit will typically read slightly higher than FLIM micr
|
|
|
285
300
|
|
|
286
301
|
## Acknowledgements
|
|
287
302
|
|
|
288
|
-
FLIMKit is designed, developed, and maintained by Alex Hunt. Anthropic's Claude AI was used as an assistant for parts of the GUI implementation, compiled app builds, code debugging, and Docker packaging; all scientific design, fitting/phasor methods, validation, and the overall architecture are the author's own work.
|
|
303
|
+
FLIMKit is designed, developed, and maintained by Alex Hunt. Anthropic's Claude AI was used as an assistant for parts of the GUI implementation, the add-on system, compiled app builds, code debugging, and Docker packaging; all scientific design, fitting/phasor methods, validation, and the overall architecture are the author's own work.
|
|
289
304
|
|
|
290
305
|
Contributions by Zhen Yuan were developed with assistance from OpenAI's GPT-5.6 Sol, operated through Hermes Agent by Nous Research. This assistance supported implementation, testing, and documentation. Zhen Yuan directed and reviewed the work and remains responsible for the submitted implementations.
|
|
291
306
|
|
|
@@ -1964,6 +1964,8 @@ Anthropic's Claude AI assisted with parts of the GUI implementation.
|
|
|
1964
1964
|
variable=bv_format, value='png').pack(anchor='w', pady=3)
|
|
1965
1965
|
ttk.Radiobutton(fmt_frame, text='OME-TIFF (lossless, metadata-rich)',
|
|
1966
1966
|
variable=bv_format, value='ometiff').pack(anchor='w', pady=3)
|
|
1967
|
+
ttk.Radiobutton(fmt_frame, text='OME-Zarr (lossless, compressed, one channel per image)',
|
|
1968
|
+
variable=bv_format, value='omezarr').pack(anchor='w', pady=3)
|
|
1967
1969
|
loc_frame = ttk.LabelFrame(dlg, text='Save Location', padding=10)
|
|
1968
1970
|
loc_frame.pack(fill='x', padx=20, pady=5)
|
|
1969
1971
|
export_path = tk.StringVar(value=output_dir)
|
|
@@ -1990,11 +1992,21 @@ Anthropic's Claude AI assisted with parts of the GUI implementation.
|
|
|
1990
1992
|
messagebox.showwarning('No path', 'Please select an export directory.')
|
|
1991
1993
|
return
|
|
1992
1994
|
fmt = bv_format.get()
|
|
1995
|
+
if fmt == 'omezarr':
|
|
1996
|
+
try:
|
|
1997
|
+
import zarr
|
|
1998
|
+
except ImportError:
|
|
1999
|
+
messagebox.showerror(
|
|
2000
|
+
'Zarr not installed',
|
|
2001
|
+
'OME-Zarr export needs the zarr package.\n\n'
|
|
2002
|
+
'Install it with: pip install zarr')
|
|
2003
|
+
return
|
|
1993
2004
|
print(f'[Export] Exporting {len(selected_images)} images in {fmt.upper()} format to {export_dir}')
|
|
1994
2005
|
self._export_images(selected_images, export_dir,
|
|
1995
2006
|
with_scalebar=bv_scalebar.get(),
|
|
1996
2007
|
with_annotations=bv_annotations.get(),
|
|
1997
|
-
format=fmt
|
|
2008
|
+
format=fmt,
|
|
2009
|
+
fit_result=image_dict)
|
|
1998
2010
|
dlg.destroy()
|
|
1999
2011
|
messagebox.showinfo('Success', f'Results exported to\n{export_dir}')
|
|
2000
2012
|
except Exception as e:
|
|
@@ -2018,7 +2030,7 @@ Anthropic's Claude AI assisted with parts of the GUI implementation.
|
|
|
2018
2030
|
|
|
2019
2031
|
def _export_images(self, image_dict: dict, output_dir: str,
|
|
2020
2032
|
with_scalebar: bool = True, with_annotations: bool = True,
|
|
2021
|
-
format: str = 'png'):
|
|
2033
|
+
format: str = 'png', fit_result: dict = None):
|
|
2022
2034
|
try:
|
|
2023
2035
|
from pathlib import Path
|
|
2024
2036
|
import numpy as np
|
|
@@ -2032,6 +2044,44 @@ Anthropic's Claude AI assisted with parts of the GUI implementation.
|
|
|
2032
2044
|
if with_scalebar and pixel_size_um is None:
|
|
2033
2045
|
print('[Export] No pixel size available - scale bar will be omitted')
|
|
2034
2046
|
with_scalebar = False
|
|
2047
|
+
if fmt == 'omezarr':
|
|
2048
|
+
try:
|
|
2049
|
+
from flimkit.utils.ome_zarr import fit_metadata, write_ome_zarr
|
|
2050
|
+
planes = {k: v for k, v in image_dict.items()
|
|
2051
|
+
if isinstance(v, np.ndarray) and v.ndim == 2}
|
|
2052
|
+
skipped = [k for k, v in image_dict.items()
|
|
2053
|
+
if isinstance(v, np.ndarray) and v.ndim != 2]
|
|
2054
|
+
for k in skipped:
|
|
2055
|
+
print(f'[Export] Skipping {k}: OME-Zarr export takes 2-D maps only')
|
|
2056
|
+
if not planes:
|
|
2057
|
+
print('[Export] No 2-D images selected for OME-Zarr export')
|
|
2058
|
+
else:
|
|
2059
|
+
by_shape = {}
|
|
2060
|
+
for k, v in planes.items():
|
|
2061
|
+
by_shape.setdefault(v.shape, {})[k] = v
|
|
2062
|
+
units = {k: 'ns' for k in planes if k.startswith(('lifetime', 'tau'))}
|
|
2063
|
+
meta = fit_metadata(fit_result or image_dict, pixel_size_um,
|
|
2064
|
+
channel_units=units or None)
|
|
2065
|
+
stem = self._current_scan_stem() or 'results'
|
|
2066
|
+
for idx, (shape, group_planes) in enumerate(sorted(by_shape.items())):
|
|
2067
|
+
suffix = '' if idx == 0 else f'_{shape[0]}x{shape[1]}'
|
|
2068
|
+
output_file = output_path / f'{stem}{suffix}.ome.zarr'
|
|
2069
|
+
if output_file.exists():
|
|
2070
|
+
import shutil as _shutil
|
|
2071
|
+
_shutil.rmtree(output_file)
|
|
2072
|
+
write_ome_zarr(output_file, group_planes,
|
|
2073
|
+
pixel_size_um=pixel_size_um, metadata=meta,
|
|
2074
|
+
name=stem)
|
|
2075
|
+
channels = ', '.join(sorted(group_planes))
|
|
2076
|
+
print(f'\u2713 Exported OME-Zarr: {output_file.name} ({channels})')
|
|
2077
|
+
exported_count += 1
|
|
2078
|
+
except ImportError as e:
|
|
2079
|
+
print(f'[Export] zarr not installed ({e}), falling back to PNG')
|
|
2080
|
+
fmt = 'png'
|
|
2081
|
+
except Exception as e:
|
|
2082
|
+
print(f'[Export] Error exporting OME-Zarr: {e}')
|
|
2083
|
+
import traceback
|
|
2084
|
+
traceback.print_exc()
|
|
2035
2085
|
if fmt == 'ometiff':
|
|
2036
2086
|
try:
|
|
2037
2087
|
import tifffile
|
|
@@ -3905,6 +3955,17 @@ def discard_default_root():
|
|
|
3905
3955
|
pass
|
|
3906
3956
|
tk._default_root = None
|
|
3907
3957
|
|
|
3958
|
+
def salvage_root():
|
|
3959
|
+
root = getattr(tk, '_default_root', None)
|
|
3960
|
+
if root is None:
|
|
3961
|
+
return None
|
|
3962
|
+
try:
|
|
3963
|
+
for child in root.winfo_children():
|
|
3964
|
+
child.destroy()
|
|
3965
|
+
except tk.TclError:
|
|
3966
|
+
return None
|
|
3967
|
+
return root
|
|
3968
|
+
|
|
3908
3969
|
def plain_root():
|
|
3909
3970
|
if HAS_DND:
|
|
3910
3971
|
try:
|
|
@@ -3912,7 +3973,9 @@ def plain_root():
|
|
|
3912
3973
|
return Tk()
|
|
3913
3974
|
except Exception as exc:
|
|
3914
3975
|
print(f'Drag and drop is off, the tkdnd library would not load: {exc}')
|
|
3915
|
-
|
|
3976
|
+
salvaged = salvage_root()
|
|
3977
|
+
if salvaged is not None:
|
|
3978
|
+
return salvaged
|
|
3916
3979
|
return tk.Tk()
|
|
3917
3980
|
|
|
3918
3981
|
def apply_fallback_theme(root):
|
|
@@ -3938,17 +4001,19 @@ def launch_gui():
|
|
|
3938
4001
|
from flimkit import plugins
|
|
3939
4002
|
plugins.ensure_loaded()
|
|
3940
4003
|
themed = None
|
|
4004
|
+
root = None
|
|
3941
4005
|
if HAS_TKMT:
|
|
3942
4006
|
try:
|
|
3943
4007
|
themed = FLIMKitGUIThemed(theme='sun-valley', mode='dark')
|
|
3944
4008
|
except tk.TclError as exc:
|
|
3945
|
-
|
|
4009
|
+
root = salvage_root()
|
|
3946
4010
|
print(f'The themed window would not start on this Tk: {exc}')
|
|
3947
4011
|
print('Falling back. Tk 9 needs tkinterdnd2 0.6.2 or newer and sv-ttk.')
|
|
3948
4012
|
if themed is not None:
|
|
3949
4013
|
themed.run(cleanresize=False)
|
|
3950
4014
|
return
|
|
3951
|
-
root
|
|
4015
|
+
if root is None:
|
|
4016
|
+
root = plain_root()
|
|
3952
4017
|
print(f'Theme: {apply_fallback_theme(root)}')
|
|
3953
4018
|
FLIMKitGUIFallback(root)
|
|
3954
4019
|
root.mainloop()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
__version__ = '0.13.
|
|
1
|
+
__version__ = '0.13.4'
|
|
2
2
|
fitter_version = '20'
|
|
3
3
|
|
|
4
4
|
roadmap = '''Flim program roadmap:
|
|
@@ -41,5 +41,7 @@ Need to add Phasor once added to the codebase, and update version to 1.0. And ad
|
|
|
41
41
|
0.13.0 -> per-pixel fitting numbers move, so fitter_version goes to 20. Free-tau per-pixel fits carry their lifetimes in nanoseconds rather than seconds, which roughly halves the error against known truth and makes the CPU, CUDA and Metal paths agree to every digit reported. Pile-up correction was silently discarded on the free-tau path and now runs, and pile-up can instead go in the per-pixel forward model with --pileup-in-model, which leaves the data Poisson. The background can be fitted as a term in the model rather than subtracted, with --bg-in-model, off by default because the two routes are biased in opposite directions rather than one being better. Per-pixel chi-squared is computed in float32, which takes a 512 square fit from 2.02s to 1.50s and the test suite from 280s to 155s. The GPU block budget is per backend, 32 MB on CUDA and ROCm against 256 MB on MLX, worth a flat 2x on an A5000, and is clamped to half of free device memory.
|
|
42
42
|
0.13.1 -> documentation fixes and two things found on a first run against QuPath on a fresh install. Stitching says how big the photon cube is, where it is going and how long the allocation took, and warns when the output drive cannot make sparse files, because exFAT writes the whole 57.2 GB before the first tile is read. flimkit[cellpose] works as well as flimkit[segmentation]. The docs name flimkit-bridge, say which QuPath pipeline writes the photon cube, document FLIMKIT_GPU_BLOCK_BYTES, and drop two claims that were no longer true: that the GPU backends cannot honour a fit window, and that free-tau per-pixel fitting uses batched Adam on the GPU. No fitted number changes, so fitter_version stays at 20.
|
|
43
43
|
0.13.2 -> the desktop GUI starts on a Tk 9 interpreter. The Windows python.org build of Python 3.14 ships Tk 9.0 where the macOS build still ships 8.6, and two GUI dependencies were built for Tcl 8.6 only, so the window died before it opened with either an unloadable tkdnd library or an invalid command name set_theme, depending on what was installed. tkinterdnd2 moves to 0.6.2 or newer and sv-ttk joins the gui extra, both of which load on Tk 8.6 and Tk 9. FLIMKit also falls back to a plain window rather than dying when a theme or a Tcl package fails at run time, which it could not do before because it chose its path from whether the imports had worked and both libraries import cleanly before failing inside Tcl. No fitted number changes.
|
|
44
|
+
0.13.3 -> OME-Zarr joins PNG and OME-TIFF in the image export. One store per export, named after the scan, holding a single (c, y, x) float32 array with every exported map as a named channel, so intensity and lifetime are two channels of one image rather than two files. The pixel size and the summed-fit summary go into the store metadata, and the pixel size also fills the NGFF scale transform, so exported maps carry their calibration and their fit numbers. Chunks are compressed with Blosc/zstd, which puts a 512 square float32 intensity and lifetime pair at about a third of the two uncompressed OME-TIFFs. zarr is a base dependency rather than an extra, so the pip install and the compiled application both carry it. No fitted number changes, so fitter_version stays at 20.
|
|
45
|
+
0.13.4 -> the desktop GUI no longer dies on a Tk 9 interpreter when the Sun Valley theme fails to load. Tk 9 on macOS queues an idle handler when a root window is created and does not cancel it when that root is destroyed, so the fallback path, which threw the themed root away and built a second one, segfaulted inside showRootWindow on the first update_idletasks rather than opening a window. The fallback now reuses the root the themed attempt had already built, clearing its children, because only the theme had failed and the window itself was sound. Tk 8.6 survived the destroy and rebuild, which is why this only showed up on a fresh install. No fitted number changes, so fitter_version stays at 20.
|
|
44
46
|
1.0.0 -> release version with all core features implemented and tested
|
|
45
47
|
1.5.0 (or 2.0) -> validation of fitting results with known fluorophores.'''
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
NGFF_VERSION = '0.4'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _to_jsonable(value):
|
|
8
|
+
if isinstance(value, dict):
|
|
9
|
+
return {str(k): _to_jsonable(v) for k, v in value.items()}
|
|
10
|
+
if isinstance(value, (list, tuple)):
|
|
11
|
+
return [_to_jsonable(v) for v in value]
|
|
12
|
+
if isinstance(value, np.ndarray):
|
|
13
|
+
return _to_jsonable(value.tolist())
|
|
14
|
+
if isinstance(value, np.generic):
|
|
15
|
+
value = value.item()
|
|
16
|
+
if isinstance(value, float) and not np.isfinite(value):
|
|
17
|
+
return None
|
|
18
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
19
|
+
return value
|
|
20
|
+
return str(value)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _open_group(path):
|
|
24
|
+
import zarr
|
|
25
|
+
try:
|
|
26
|
+
return zarr.open_group(store=str(path), mode='w', zarr_format=2)
|
|
27
|
+
except TypeError:
|
|
28
|
+
return zarr.open_group(str(path), mode='w')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _compressor():
|
|
32
|
+
try:
|
|
33
|
+
from numcodecs import Blosc
|
|
34
|
+
except ImportError:
|
|
35
|
+
return None
|
|
36
|
+
return Blosc(cname='zstd', clevel=5, shuffle=Blosc.SHUFFLE)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _write_array(group, name, data, chunks):
|
|
40
|
+
codec = _compressor()
|
|
41
|
+
if hasattr(group, 'create_array'):
|
|
42
|
+
kwargs = {'compressors': codec} if codec is not None else {}
|
|
43
|
+
arr = group.create_array(name=name, shape=data.shape, dtype=data.dtype,
|
|
44
|
+
chunks=chunks, **kwargs)
|
|
45
|
+
arr[:] = data
|
|
46
|
+
return arr
|
|
47
|
+
kwargs = {'compressor': codec} if codec is not None else {}
|
|
48
|
+
return group.create_dataset(name, data=data, chunks=chunks, **kwargs)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _channel_window(plane):
|
|
52
|
+
finite = plane[np.isfinite(plane)]
|
|
53
|
+
if finite.size == 0:
|
|
54
|
+
return 0.0, 1.0
|
|
55
|
+
lo = float(np.min(finite))
|
|
56
|
+
hi = float(np.max(finite))
|
|
57
|
+
if hi <= lo:
|
|
58
|
+
hi = lo + 1.0
|
|
59
|
+
return lo, hi
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def write_ome_zarr(path, channels, pixel_size_um=None, metadata=None, name='flimkit'):
|
|
63
|
+
if not channels:
|
|
64
|
+
raise ValueError('write_ome_zarr needs at least one channel')
|
|
65
|
+
labels = list(channels)
|
|
66
|
+
shapes = {np.asarray(channels[k]).shape[:2] for k in labels}
|
|
67
|
+
if len(shapes) != 1:
|
|
68
|
+
raise ValueError(f'all channels must share one shape, got {sorted(shapes)}')
|
|
69
|
+
ny, nx = shapes.pop()
|
|
70
|
+
stack = np.empty((len(labels), ny, nx), dtype=np.float32)
|
|
71
|
+
windows = []
|
|
72
|
+
for idx, label in enumerate(labels):
|
|
73
|
+
plane = np.asarray(channels[label], dtype=np.float32)
|
|
74
|
+
if plane.ndim == 3:
|
|
75
|
+
plane = plane.mean(axis=2)
|
|
76
|
+
stack[idx] = plane
|
|
77
|
+
windows.append(_channel_window(plane))
|
|
78
|
+
path = Path(path)
|
|
79
|
+
group = _open_group(path)
|
|
80
|
+
chunks = (1, min(ny, 512), min(nx, 512))
|
|
81
|
+
_write_array(group, '0', stack, chunks)
|
|
82
|
+
scale = [1.0, 1.0, 1.0]
|
|
83
|
+
axes = [{'name': 'c', 'type': 'channel'},
|
|
84
|
+
{'name': 'y', 'type': 'space'},
|
|
85
|
+
{'name': 'x', 'type': 'space'}]
|
|
86
|
+
if pixel_size_um and pixel_size_um > 0:
|
|
87
|
+
scale = [1.0, float(pixel_size_um), float(pixel_size_um)]
|
|
88
|
+
axes[1]['unit'] = 'micrometer'
|
|
89
|
+
axes[2]['unit'] = 'micrometer'
|
|
90
|
+
attrs = {
|
|
91
|
+
'multiscales': [{
|
|
92
|
+
'version': NGFF_VERSION,
|
|
93
|
+
'name': name,
|
|
94
|
+
'axes': axes,
|
|
95
|
+
'datasets': [{'path': '0',
|
|
96
|
+
'coordinateTransformations': [{'type': 'scale', 'scale': scale}]}],
|
|
97
|
+
}],
|
|
98
|
+
'omero': {
|
|
99
|
+
'name': name,
|
|
100
|
+
'version': NGFF_VERSION,
|
|
101
|
+
'rdefs': {'model': 'greyscale'},
|
|
102
|
+
'channels': [{'label': label,
|
|
103
|
+
'color': 'FFFFFF',
|
|
104
|
+
'active': True,
|
|
105
|
+
'window': {'start': lo, 'end': hi, 'min': lo, 'max': hi}}
|
|
106
|
+
for label, (lo, hi) in zip(labels, windows)],
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
if metadata:
|
|
110
|
+
attrs['flimkit'] = _to_jsonable(metadata)
|
|
111
|
+
group.attrs.update(attrs)
|
|
112
|
+
return path
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def fit_metadata(fit_result, pixel_size_um=None, channel_units=None):
|
|
116
|
+
meta = {}
|
|
117
|
+
summary = (fit_result or {}).get('global_summary')
|
|
118
|
+
if isinstance(summary, dict):
|
|
119
|
+
meta['global_summary'] = summary
|
|
120
|
+
for key in ('n_exp', 'model', 'suggested_binning', 'tau_weighting', 'irf_source'):
|
|
121
|
+
if fit_result and key in fit_result and not isinstance(fit_result[key], np.ndarray):
|
|
122
|
+
meta[key] = fit_result[key]
|
|
123
|
+
if pixel_size_um and pixel_size_um > 0:
|
|
124
|
+
meta['pixel_size_um'] = float(pixel_size_um)
|
|
125
|
+
if channel_units:
|
|
126
|
+
meta['channel_units'] = channel_units
|
|
127
|
+
return meta
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flimkit
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.4
|
|
4
4
|
Summary: Fluorescence lifetime imaging analysis: reading, fitting, phasors and stitching
|
|
5
5
|
Author-email: Alex Hunt <alexander.hunt@ed.ac.uk>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -38,6 +38,7 @@ Requires-Dist: shapely==2.1.2
|
|
|
38
38
|
Requires-Dist: tifffile
|
|
39
39
|
Requires-Dist: tqdm==4.67.3
|
|
40
40
|
Requires-Dist: xarray==2026.1.0
|
|
41
|
+
Requires-Dist: zarr>=2.18
|
|
41
42
|
Provides-Extra: gui
|
|
42
43
|
Requires-Dist: tkinterdnd2>=0.6.2; extra == "gui"
|
|
43
44
|
Requires-Dist: TKinterModernThemes; extra == "gui"
|
|
@@ -86,9 +87,25 @@ Desktop GUI showing reconvolution fitting with per-pixel lifetime maps, summed d
|
|
|
86
87
|
|
|
87
88
|
## Installation
|
|
88
89
|
|
|
90
|
+
### No Python, no terminal
|
|
91
|
+
|
|
92
|
+
Download the build for your machine from the
|
|
93
|
+
[Releases](https://github.com/FLIMKit/FLIMKit/releases/latest) tab, unzip it,
|
|
94
|
+
and run it. Python is inside it, so there is nothing else to install.
|
|
95
|
+
`FLIMKit-windows.zip`, `FLIMKit-macos.zip` and `FLIMKit-linux.zip` are attached
|
|
96
|
+
to every release.
|
|
97
|
+
|
|
98
|
+
On macOS the app is self-signed, so the first launch needs right-click then
|
|
99
|
+
Open. A double-click will be refused.
|
|
100
|
+
|
|
101
|
+
This route gets the desktop application only. Everything below is for scripting,
|
|
102
|
+
the terminal, or GPU fitting.
|
|
103
|
+
|
|
104
|
+
### From PyPI
|
|
105
|
+
|
|
89
106
|
Python ≥ 3.12 required (3.14 recommended, official builds use 3.14).
|
|
90
107
|
|
|
91
|
-
|
|
108
|
+
For analysis in scripts, notebooks and the terminal:
|
|
92
109
|
|
|
93
110
|
```bash
|
|
94
111
|
pip install flimkit # readers, fitting, phasors, stitching, the CLI
|
|
@@ -114,8 +131,6 @@ For development work (PyInstaller + test dependencies):
|
|
|
114
131
|
python install.py --dev
|
|
115
132
|
```
|
|
116
133
|
|
|
117
|
-
Or download the compiled app from the Releases tab (no Python needed).
|
|
118
|
-
|
|
119
134
|
## Docker / TrueNAS SCALE
|
|
120
135
|
|
|
121
136
|
A pre-built image is available on Docker Hub. It runs the full desktop GUI in a browser via xpra (no installation needed on the client).
|
|
@@ -310,6 +325,7 @@ python run_tests.py integration # integration tests only
|
|
|
310
325
|
|---|---|
|
|
311
326
|
| PNG | Intensity and lifetime map images |
|
|
312
327
|
| OME-TIFF | Lossless export with metadata - opens in Fiji/ImageJ |
|
|
328
|
+
| OME-Zarr | Lossless and compressed, one channel per map (OME-NGFF 0.4) |
|
|
313
329
|
| GeoJSON | ROI geometries and stats - imports directly into QuPath |
|
|
314
330
|
| CSV | Fit summaries and per-ROI statistics |
|
|
315
331
|
| NPZ | Session files for restoring analysis state |
|
|
@@ -346,7 +362,7 @@ Fitted lifetimes from FLIMKit will typically read slightly higher than FLIM micr
|
|
|
346
362
|
|
|
347
363
|
## Acknowledgements
|
|
348
364
|
|
|
349
|
-
FLIMKit is designed, developed, and maintained by Alex Hunt. Anthropic's Claude AI was used as an assistant for parts of the GUI implementation, compiled app builds, code debugging, and Docker packaging; all scientific design, fitting/phasor methods, validation, and the overall architecture are the author's own work.
|
|
365
|
+
FLIMKit is designed, developed, and maintained by Alex Hunt. Anthropic's Claude AI was used as an assistant for parts of the GUI implementation, the add-on system, compiled app builds, code debugging, and Docker packaging; all scientific design, fitting/phasor methods, validation, and the overall architecture are the author's own work.
|
|
350
366
|
|
|
351
367
|
Contributions by Zhen Yuan were developed with assistance from OpenAI's GPT-5.6 Sol, operated through Hermes Agent by Nous Research. This assistance supported implementation, testing, and documentation. Zhen Yuan directed and reviewed the work and remains responsible for the submitted implementations.
|
|
352
368
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|