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.
Files changed (110) hide show
  1. {flimkit-0.13.2 → flimkit-0.13.4}/PKG-INFO +21 -5
  2. {flimkit-0.13.2 → flimkit-0.13.4}/README.md +19 -4
  3. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/gui.py +70 -5
  4. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/_version.py +3 -1
  5. flimkit-0.13.4/flimkit/utils/ome_zarr.py +127 -0
  6. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit.egg-info/PKG-INFO +21 -5
  7. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit.egg-info/SOURCES.txt +1 -0
  8. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit.egg-info/requires.txt +1 -0
  9. {flimkit-0.13.2 → flimkit-0.13.4}/pyproject.toml +1 -0
  10. {flimkit-0.13.2 → flimkit-0.13.4}/LICENSE.md +0 -0
  11. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/FLIM/__init__.py +0 -0
  12. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/FLIM/assemble.py +0 -0
  13. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/FLIM/batch.py +0 -0
  14. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/FLIM/bg_tools.py +0 -0
  15. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/FLIM/fit_tools.py +0 -0
  16. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/FLIM/fitters.py +0 -0
  17. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/FLIM/irf_tools.py +0 -0
  18. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/FLIM/models.py +0 -0
  19. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/GPU/__init__.py +0 -0
  20. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/GPU/_base.py +0 -0
  21. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/GPU/cuda.py +0 -0
  22. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/GPU/mlx_backend.py +0 -0
  23. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/GPU/mps.py +0 -0
  24. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/GPU/rocm.py +0 -0
  25. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/GPU/torch_backend.py +0 -0
  26. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/app_state.py +0 -0
  27. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/controller.py +0 -0
  28. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/expert_settings.py +0 -0
  29. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/fit_help.py +0 -0
  30. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/fov_preview.py +0 -0
  31. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/icon.icns +0 -0
  32. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/icon.ico +0 -0
  33. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/icon.png +0 -0
  34. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/irf_widget.py +0 -0
  35. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/mode_controller.py +0 -0
  36. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/modes/__init__.py +0 -0
  37. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/modes/base.py +0 -0
  38. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/modes/batch_mode.py +0 -0
  39. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/modes/fov_mode.py +0 -0
  40. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/modes/irf_mode.py +0 -0
  41. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/modes/phasor_mode.py +0 -0
  42. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/modes/stitch_mode.py +0 -0
  43. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/phasor_panel.py +0 -0
  44. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/progress_window.py +0 -0
  45. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/project_panel.py +0 -0
  46. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/results_panel.py +0 -0
  47. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/roi_tools.py +0 -0
  48. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/UI/utils.py +0 -0
  49. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/__init__.py +0 -0
  50. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/cli.py +0 -0
  51. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/configs.py +0 -0
  52. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/dialogs.py +0 -0
  53. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/BH/__init__.py +0 -0
  54. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/BH/reader.py +0 -0
  55. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/BH/writer.py +0 -0
  56. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/ISS/__init__.py +0 -0
  57. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/ISS/fdflim.py +0 -0
  58. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/ISS/image.py +0 -0
  59. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/ISS/reader.py +0 -0
  60. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/PS/__init__.py +0 -0
  61. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/PS/reader.py +0 -0
  62. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/PTU/__init__.py +0 -0
  63. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/PTU/decode.py +0 -0
  64. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/PTU/phu.py +0 -0
  65. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/PTU/reader.py +0 -0
  66. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/PTU/series.py +0 -0
  67. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/PTU/stitch.py +0 -0
  68. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/PTU/tools.py +0 -0
  69. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/__init__.py +0 -0
  70. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/flim_file.py +0 -0
  71. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/phasor.py +0 -0
  72. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/formats/signal.py +0 -0
  73. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/image/tools.py +0 -0
  74. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/interactive.py +0 -0
  75. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/mpl_backend.py +0 -0
  76. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/phasor/__init__.py +0 -0
  77. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/phasor/filters.py +0 -0
  78. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/phasor/fret.py +0 -0
  79. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/phasor/interactive.py +0 -0
  80. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/phasor/peaks.py +0 -0
  81. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/phasor/signal.py +0 -0
  82. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/phasor_launcher.py +0 -0
  83. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/plugins/__init__.py +0 -0
  84. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/plugins/bindings.py +0 -0
  85. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/plugins/builtin/__init__.py +0 -0
  86. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/plugins/builtin/core_tools.py +0 -0
  87. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/plugins/loader.py +0 -0
  88. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/plugins/registry.py +0 -0
  89. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/project.py +0 -0
  90. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/synth.py +0 -0
  91. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/__init__.py +0 -0
  92. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/batch_fit.py +0 -0
  93. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/config_manager.py +0 -0
  94. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/config_snapshot.py +0 -0
  95. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/crash_handler.py +0 -0
  96. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/display.py +0 -0
  97. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/enhanced_outputs.py +0 -0
  98. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/fancy.py +0 -0
  99. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/lifetime_image.py +0 -0
  100. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/misc.py +0 -0
  101. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/plotting.py +0 -0
  102. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/roi.py +0 -0
  103. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/session.py +0 -0
  104. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/update_check.py +0 -0
  105. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/xlsx_tools.py +0 -0
  106. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit/utils/xml_utils.py +0 -0
  107. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit.egg-info/dependency_links.txt +0 -0
  108. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit.egg-info/entry_points.txt +0 -0
  109. {flimkit-0.13.2 → flimkit-0.13.4}/flimkit.egg-info/top_level.txt +0 -0
  110. {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.2
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
- From PyPI, for analysis in scripts, notebooks and the terminal:
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
- From PyPI, for analysis in scripts, notebooks and the terminal:
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
- discard_default_root()
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
- discard_default_root()
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 = plain_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.2'
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.2
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
- From PyPI, for analysis in scripts, notebooks and the terminal:
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
 
@@ -99,6 +99,7 @@ flimkit/utils/enhanced_outputs.py
99
99
  flimkit/utils/fancy.py
100
100
  flimkit/utils/lifetime_image.py
101
101
  flimkit/utils/misc.py
102
+ flimkit/utils/ome_zarr.py
102
103
  flimkit/utils/plotting.py
103
104
  flimkit/utils/roi.py
104
105
  flimkit/utils/session.py
@@ -20,6 +20,7 @@ shapely==2.1.2
20
20
  tifffile
21
21
  tqdm==4.67.3
22
22
  xarray==2026.1.0
23
+ zarr>=2.18
23
24
 
24
25
  [all]
25
26
  flimkit[gui,notebook,segmentation]
@@ -45,6 +45,7 @@ dependencies = [
45
45
  'tifffile',
46
46
  'tqdm==4.67.3',
47
47
  'xarray==2026.1.0',
48
+ 'zarr>=2.18',
48
49
  ]
49
50
 
50
51
  [project.optional-dependencies]
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