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,345 @@
1
+ import numpy as np
2
+ import tifffile
3
+ from pathlib import Path
4
+
5
+
6
+ def save_fit_summary_txt(
7
+ summary,
8
+ output_path,
9
+ n_exp=2,
10
+ strategy='gaussian',
11
+ metadata=None
12
+ ):
13
+ output_path = Path(output_path)
14
+
15
+ with open(output_path, 'w') as f:
16
+ f.write('='*60 + '\n')
17
+ f.write('FLIM FIT RESULTS SUMMARY\n')
18
+ f.write('='*60 + '\n\n')
19
+
20
+ # Metadata
21
+ if metadata:
22
+ f.write('ROI Information:\n')
23
+ f.write('-'*60 + '\n')
24
+ if 'canvas_shape' in metadata:
25
+ f.write(f"Canvas size: {metadata['canvas_shape'][0]} × {metadata['canvas_shape'][1]} pixels\n")
26
+ if 'tiles_processed' in metadata:
27
+ f.write(f"Tiles processed: {metadata['tiles_processed']}\n")
28
+ if 'total_photons' in metadata:
29
+ f.write(f"Total photons: {metadata['total_photons']:,.0f}\n")
30
+ f.write('\n')
31
+
32
+ # Fit parameters
33
+ f.write('Fit Parameters:\n')
34
+ f.write('-'*60 + '\n')
35
+ f.write(f"Number of exponentials: {n_exp}\n")
36
+ f.write(f"IRF strategy: {strategy}\n")
37
+ f.write(f"Chi-squared (reduced): {summary.get('chi2r', 0):.6f}\n")
38
+ f.write(f"Background: {summary.get('bg', 0):.3f}\n")
39
+ if summary.get('tvb_scale', 0):
40
+ f.write(f"TVB scale: {summary['tvb_scale']:.3f}\n")
41
+ if 'sigma' in summary:
42
+ f.write(f"IRF broadening (σ): {summary['sigma']:.3f} ns\n")
43
+ f.write('\n')
44
+
45
+ # Lifetime components
46
+ f.write('Lifetime Components:\n')
47
+ f.write('-'*60 + '\n')
48
+ for i in range(1, n_exp + 1):
49
+ tau_key = f'tau_{i}'
50
+ amp_key = f'a{i}'
51
+
52
+ if tau_key in summary:
53
+ tau = summary[tau_key]
54
+ f.write(f"Component {i}:\n")
55
+ f.write(f" τ{i} = {tau:.4f} ns\n")
56
+
57
+ if amp_key in summary:
58
+ amp = summary[amp_key]
59
+ f.write(f" A{i} = {amp:.4f}\n")
60
+
61
+ # Calculate fractional intensity
62
+ total_amp = sum(summary.get(f'a{j}', 0) for j in range(1, n_exp + 1))
63
+ if total_amp > 0:
64
+ frac = amp / total_amp * 100
65
+ f.write(f" Fractional intensity: {frac:.1f}%\n")
66
+
67
+ f.write('\n')
68
+
69
+ # Average lifetime
70
+ if n_exp > 1:
71
+ tau_avg = 0
72
+ total_amp = 0
73
+ for i in range(1, n_exp + 1):
74
+ tau = summary.get(f'tau_{i}', 0)
75
+ amp = summary.get(f'a{i}', 0)
76
+ tau_avg += tau * amp
77
+ total_amp += amp
78
+
79
+ if total_amp > 0:
80
+ tau_avg /= total_amp
81
+ f.write(f"Average lifetime (amplitude-weighted): {tau_avg:.4f} ns\n\n")
82
+
83
+ # All parameters (raw)
84
+ f.write('Raw Parameters:\n')
85
+ f.write('-'*60 + '\n')
86
+ for key, value in sorted(summary.items()):
87
+ if isinstance(value, (int, float)):
88
+ f.write(f"{key}: {value:.6e}\n")
89
+ else:
90
+ f.write(f"{key}: {value}\n")
91
+
92
+ f.write('\n' + '='*60 + '\n')
93
+
94
+ print(f"Fit summary saved: {output_path}")
95
+
96
+
97
+ def save_weighted_tau_images(
98
+ pixel_maps,
99
+ output_dir,
100
+ roi_name='ROI',
101
+ n_exp=2,
102
+ save_intensity=True,
103
+ save_amplitude=True,
104
+ tau_display_min=None,
105
+ tau_display_max=None,
106
+ intensity_display_min=None,
107
+ intensity_display_max=None,
108
+ target_shape=None,
109
+ ):
110
+ output_dir = Path(output_dir)
111
+ output_dir.mkdir(parents=True, exist_ok=True)
112
+
113
+ # stitch_flim_tiles saves *_stitched_intensity.tif at full (H, W).
114
+ # fit_per_pixel with binning>1 returns maps at (H//b, W//b).
115
+ # Nearest-neighbour resize corrects the mismatch without blurring values.
116
+ if target_shape is not None:
117
+ th, tw = int(target_shape[0]), int(target_shape[1])
118
+ try:
119
+ import cv2 as _cv2
120
+ def _up(a):
121
+ if a is None or a.ndim < 2 or a.shape[:2] == (th, tw): return a
122
+ return _cv2.resize(a.astype(np.float32), (tw, th),
123
+ interpolation=_cv2.INTER_NEAREST)
124
+ except ImportError:
125
+ def _up(a):
126
+ if a is None or a.ndim < 2 or a.shape[:2] == (th, tw): return a
127
+ h, w = a.shape[:2]
128
+ rh, rw = max(1, round(th / h)), max(1, round(tw / w))
129
+ return np.repeat(np.repeat(a, rh, axis=0), rw, axis=1)[:th, :tw]
130
+ pixel_maps = {k: _up(v) for k, v in pixel_maps.items()}
131
+ print(f" \u2191 Pixel maps upsampled to {th}\xd7{tw} px")
132
+ # Get intensity map (total photon counts per pixel)
133
+ if 'intensity' in pixel_maps:
134
+ intensity = pixel_maps['intensity']
135
+ else:
136
+ # Calculate from amplitudes if not provided
137
+ intensity = np.zeros_like(pixel_maps['tau_1'])
138
+ for i in range(1, n_exp + 1):
139
+ amp_key = f'a{i}'
140
+ if amp_key in pixel_maps:
141
+ intensity += pixel_maps[amp_key]
142
+
143
+ # Save intensity image - scaled to full uint16 range like stitch-only
144
+ if save_intensity:
145
+ intensity_out = intensity.copy().astype(np.float64)
146
+ # Apply intensity display range (clip to boundaries, FLIM microscope style)
147
+ if intensity_display_min is not None:
148
+ intensity_out = np.where(
149
+ intensity_out > 0,
150
+ np.clip(intensity_out, intensity_display_min, intensity_out),
151
+ intensity_out,
152
+ )
153
+ if intensity_display_max is not None:
154
+ intensity_out = np.where(
155
+ intensity_out > 0,
156
+ np.clip(intensity_out, intensity_out, intensity_display_max),
157
+ intensity_out,
158
+ )
159
+ intensity_path = output_dir / f"{roi_name}_intensity.tif"
160
+ max_val = intensity_out.max()
161
+ if max_val > 0:
162
+ intensity_scaled = (intensity_out / max_val * 65535).astype(np.uint16)
163
+ else:
164
+ intensity_scaled = np.zeros_like(intensity_out, dtype=np.uint16)
165
+ tifffile.imwrite(str(intensity_path), intensity_scaled)
166
+ print(f"Intensity image saved: {intensity_path} (uint16, max-scaled)")
167
+ if intensity_display_min is not None or intensity_display_max is not None:
168
+ lo = intensity_display_min if intensity_display_min is not None else 'auto'
169
+ hi = intensity_display_max if intensity_display_max is not None else 'auto'
170
+ print(f" Intensity display range: [{lo}, {hi}] (clipped)")
171
+
172
+ # Compute intensity-weighted average lifetime
173
+ if save_intensity and n_exp > 1:
174
+ tau_intensity_weighted = np.zeros_like(intensity, dtype=np.float32)
175
+
176
+ for i in range(1, n_exp + 1):
177
+ tau_key = f'tau_{i}'
178
+ amp_key = f'a{i}'
179
+
180
+ if tau_key in pixel_maps and amp_key in pixel_maps:
181
+ tau = pixel_maps[tau_key]
182
+ amp = pixel_maps[amp_key]
183
+
184
+ # Weight by amplitude (which represents intensity contribution)
185
+ tau_intensity_weighted += tau * amp
186
+
187
+ # Normalize by total intensity
188
+ mask = intensity > 0
189
+ tau_intensity_weighted[mask] /= intensity[mask]
190
+ tau_intensity_weighted[~mask] = 0
191
+
192
+ # Apply lifetime display range (clip to boundaries, FLIM microscope style)
193
+ if tau_display_min is not None or tau_display_max is not None:
194
+ lo = tau_display_min if tau_display_min is not None else tau_intensity_weighted[mask].min() if mask.any() else 0
195
+ hi = tau_display_max if tau_display_max is not None else tau_intensity_weighted[mask].max() if mask.any() else 0
196
+ tau_intensity_weighted[mask] = np.clip(tau_intensity_weighted[mask], lo, hi)
197
+
198
+ # Save
199
+ tau_int_path = output_dir / f"{roi_name}_tau_intensity_weighted.tif"
200
+ tifffile.imwrite(str(tau_int_path), tau_intensity_weighted)
201
+ print(f"Intensity-weighted tau image saved: {tau_int_path}")
202
+ if mask.any():
203
+ print(f" Range: {tau_intensity_weighted[mask].min():.3f} - {tau_intensity_weighted[mask].max():.3f} ns")
204
+ if tau_display_min is not None or tau_display_max is not None:
205
+ lo_s = f"{tau_display_min}" if tau_display_min is not None else 'auto'
206
+ hi_s = f"{tau_display_max}" if tau_display_max is not None else 'auto'
207
+ print(f" Tau display range: [{lo_s}, {hi_s}] ns (clipped)")
208
+ else:
209
+ print(f" Range: no valid pixels")
210
+
211
+ # Compute amplitude-weighted average lifetime
212
+ if save_amplitude and n_exp > 1:
213
+ tau_amplitude_weighted = np.zeros_like(intensity, dtype=np.float32)
214
+ total_amplitude = np.zeros_like(intensity, dtype=np.float32)
215
+
216
+ for i in range(1, n_exp + 1):
217
+ tau_key = f'tau_{i}'
218
+ amp_key = f'a{i}'
219
+
220
+ if tau_key in pixel_maps and amp_key in pixel_maps:
221
+ tau = pixel_maps[tau_key]
222
+ amp = pixel_maps[amp_key]
223
+
224
+ # Weight by amplitude (fractional contribution)
225
+ tau_amplitude_weighted += tau * amp
226
+ total_amplitude += amp
227
+
228
+ # Normalize by total amplitude
229
+ mask = total_amplitude > 0
230
+ tau_amplitude_weighted[mask] /= total_amplitude[mask]
231
+ tau_amplitude_weighted[~mask] = 0
232
+
233
+ # Apply lifetime display range (clip to boundaries, FLIM microscope style)
234
+ if tau_display_min is not None or tau_display_max is not None:
235
+ lo = tau_display_min if tau_display_min is not None else tau_amplitude_weighted[mask].min() if mask.any() else 0
236
+ hi = tau_display_max if tau_display_max is not None else tau_amplitude_weighted[mask].max() if mask.any() else 0
237
+ tau_amplitude_weighted[mask] = np.clip(tau_amplitude_weighted[mask], lo, hi)
238
+
239
+ # Save
240
+ tau_amp_path = output_dir / f"{roi_name}_tau_amplitude_weighted.tif"
241
+ tifffile.imwrite(str(tau_amp_path), tau_amplitude_weighted)
242
+ print(f"Amplitude-weighted tau image saved: {tau_amp_path}")
243
+ if mask.any():
244
+ print(f" Range: {tau_amplitude_weighted[mask].min():.3f} - {tau_amplitude_weighted[mask].max():.3f} ns")
245
+ if tau_display_min is not None or tau_display_max is not None:
246
+ lo_s = f"{tau_display_min}" if tau_display_min is not None else 'auto'
247
+ hi_s = f"{tau_display_max}" if tau_display_max is not None else 'auto'
248
+ print(f" Tau display range: [{lo_s}, {hi_s}] ns (clipped)")
249
+ else:
250
+ print(f" Range: no valid pixels")
251
+
252
+ # For single exponential, just save the single tau map
253
+ if n_exp == 1:
254
+ tau_single = pixel_maps['tau_1']
255
+ tau_path = output_dir / f"{roi_name}_tau.tif"
256
+ tifffile.imwrite(str(tau_path), tau_single.astype(np.float32))
257
+ print(f"Lifetime image saved: {tau_path}")
258
+ mask = tau_single > 0
259
+ if mask.any():
260
+ print(f" Range: {tau_single[mask].min():.3f} - {tau_single[mask].max():.3f} ns")
261
+ else:
262
+ print(f" Range: no valid pixels")
263
+
264
+
265
+ def save_individual_tau_maps(
266
+ pixel_maps,
267
+ output_dir,
268
+ roi_name='ROI',
269
+ n_exp=2
270
+ ):
271
+ output_dir = Path(output_dir)
272
+ output_dir.mkdir(parents=True, exist_ok=True)
273
+
274
+ for i in range(1, n_exp + 1):
275
+ # Save tau map
276
+ tau_key = f'tau_{i}'
277
+ if tau_key in pixel_maps:
278
+ tau = pixel_maps[tau_key]
279
+ tau_path = output_dir / f"{roi_name}_tau{i}.tif"
280
+ tifffile.imwrite(str(tau_path), tau.astype(np.float32))
281
+ print(f"τ{i} map saved: {tau_path}")
282
+
283
+ # Save amplitude map
284
+ amp_key = f'a{i}'
285
+ if amp_key in pixel_maps:
286
+ amp = pixel_maps[amp_key]
287
+ amp_path = output_dir / f"{roi_name}_a{i}.tif"
288
+ tifffile.imwrite(str(amp_path), amp.astype(np.float32))
289
+ print(f"A{i} map saved: {amp_path}")
290
+
291
+ if 'tvb_scale' in pixel_maps:
292
+ tvb_path = output_dir / f"{roi_name}_tvb_scale.tif"
293
+ tifffile.imwrite(str(tvb_path), pixel_maps['tvb_scale'].astype(np.float32))
294
+ print(f"TVB scale map saved: {tvb_path}")
295
+
296
+
297
+ def create_complete_output_package(
298
+ summary,
299
+ pixel_maps,
300
+ output_dir,
301
+ roi_name,
302
+ n_exp,
303
+ strategy,
304
+ metadata=None,
305
+ save_individual_components=True,
306
+ tau_display_min=None,
307
+ tau_display_max=None,
308
+ intensity_display_min=None,
309
+ intensity_display_max=None,
310
+ target_shape=None,
311
+ ):
312
+ output_dir = Path(output_dir)
313
+ output_dir.mkdir(parents=True, exist_ok=True)
314
+
315
+ print(f"\n{'='*60}")
316
+ print(f"Creating complete output package: {roi_name}")
317
+ print(f"{'='*60}\n")
318
+
319
+ # Save fit summary
320
+ summary_path = output_dir / f"{roi_name}_fit_summary.txt"
321
+ save_fit_summary_txt(summary, summary_path, n_exp, strategy, metadata)
322
+
323
+ # Save pixel maps if available
324
+ if pixel_maps is not None:
325
+ # Weighted averages
326
+ save_weighted_tau_images(
327
+ pixel_maps,
328
+ output_dir,
329
+ roi_name,
330
+ n_exp,
331
+ save_intensity=True,
332
+ save_amplitude=True,
333
+ tau_display_min=tau_display_min,
334
+ tau_display_max=tau_display_max,
335
+ intensity_display_min=intensity_display_min,
336
+ intensity_display_max=intensity_display_max,
337
+ )
338
+
339
+ # Individual components
340
+ if save_individual_components:
341
+ save_individual_tau_maps(pixel_maps, output_dir, roi_name, n_exp)
342
+
343
+ print(f"\n{'='*60}")
344
+ print(f"Output package complete: {output_dir}")
345
+ print(f"{'='*60}\n")
flimkit/utils/fancy.py ADDED
@@ -0,0 +1,103 @@
1
+ import os
2
+ import time
3
+ import subprocess
4
+
5
+ def clear():
6
+ command = 'cls' if os.name == 'nt' else 'clear'
7
+ subprocess.run(command, shell=True)
8
+
9
+ class bcolors:
10
+ WARNING = '\033[93m'
11
+ ERROR = '\033[91m'
12
+ GOOD = '\033[92m'
13
+
14
+ from colorama import init, Style
15
+
16
+ init()
17
+
18
+ def reset_color():
19
+ print(Style.RESET_ALL, end='')
20
+
21
+ def display_banner():
22
+ clear()
23
+ print(r'''
24
+ ██╗ ██╗███████╗██╗ ██████╗ ██████╗ ███╗ ███╗███████╗
25
+ ██║ ██║██╔════╝██║ ██╔════╝██╔═══██╗████╗ ████║██╔════╝
26
+ ██║ █╗ ██║█████╗ ██║ ██║ ██║ ██║██╔████╔██║█████╗
27
+ ██║███╗██║██╔══╝ ██║ ██║ ██║ ██║██║╚██╔╝██║██╔══╝
28
+ ╚███╔███╔╝███████╗███████╗╚██████╗╚██████╔╝██║ ╚═╝ ██║███████╗
29
+ ╚══╝╚══╝ ╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
30
+
31
+
32
+ ''')
33
+ time.sleep(2)
34
+ clear()
35
+
36
+ def selection_program():
37
+ for x in range (0,5):
38
+ b = 'Loading' + '.' * x
39
+ print (b, end='\r')
40
+ time.sleep(1)
41
+
42
+ def flim_fitting_banner():
43
+ clear()
44
+ print(r'''
45
+ ███████╗██╗ ██╗███╗ ███╗ ███████╗██╗████████╗████████╗██╗███╗ ██╗ ██████╗
46
+ ██╔════╝██║ ██║████╗ ████║ ██╔════╝██║╚══██╔══╝╚══██╔══╝██║████╗ ██║██╔════╝
47
+ █████╗ ██║ ██║██╔████╔██║ █████╗ ██║ ██║ ██║ ██║██╔██╗ ██║██║ ███╗
48
+ ██╔══╝ ██║ ██║██║╚██╔╝██║ ██╔══╝ ██║ ██║ ██║ ██║██║╚██╗██║██║ ██║
49
+ ██║ ███████╗██║██║ ╚═╝ ██║ ██║ ██║ ██║ ██║ ██║██║ ╚████║╚██████╔╝
50
+ ╚═╝ ╚══════╝╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝
51
+
52
+ ''')
53
+ time.sleep(2)
54
+ clear()
55
+
56
+ def banner_goodbye():
57
+ clear()
58
+ print(bcolors.GOOD + r'''
59
+ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗███████╗
60
+ ██╔════╝ ██╔═══██╗██╔═══██╗██╔══██╗██╔══██╗╚██╗ ██╔╝██╔════╝
61
+ ██║ ███╗██║ ██║██║ ██║██║ ██║██████╔╝ ╚████╔╝ █████╗
62
+ ██║ ██║██║ ██║██║ ██║██║ ██║██╔══██╗ ╚██╔╝ ██╔══╝
63
+ ╚██████╔╝╚██████╔╝╚██████╔╝██████╔╝██████╔╝ ██║ ███████╗
64
+ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝
65
+
66
+
67
+ ''')
68
+ time.sleep(2)
69
+ reset_color()
70
+
71
+ def warnings_banner(text=''):
72
+ clear()
73
+ print(bcolors.WARNING + r'''
74
+
75
+ ██╗ ██╗ █████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██████╗ ██╗
76
+ ██║ ██║██╔══██╗██╔══██╗████╗ ██║██║████╗ ██║██╔════╝ ██║
77
+ ██║ █╗ ██║███████║██████╔╝██╔██╗ ██║██║██╔██╗ ██║██║ ███╗██║
78
+ ██║███╗██║██╔══██║██╔══██╗██║╚██╗██║██║██║╚██╗██║██║ ██║╚═╝
79
+ ╚███╔███╔╝██║ ██║██║ ██║██║ ╚████║██║██║ ╚████║╚██████╔╝██╗
80
+ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝
81
+
82
+
83
+
84
+ ''' )
85
+ print(text)
86
+ time.sleep(1)
87
+ reset_color()
88
+
89
+ def error_banner():
90
+ clear()
91
+ print(bcolors.ERROR + r'''
92
+
93
+ ███████╗██████╗ ██████╗ ██████╗ ██████╗ ██╗
94
+ ██╔════╝██╔══██╗██╔══██╗██╔═══██╗██╔══██╗██║
95
+ █████╗ ██████╔╝██████╔╝██║ ██║██████╔╝██║
96
+ ██╔══╝ ██╔══██╗██╔══██╗██║ ██║██╔══██╗╚═╝
97
+ ███████╗██║ ██║██║ ██║╚██████╔╝██║ ██║██╗
98
+ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝
99
+
100
+
101
+
102
+ ''')
103
+ time.sleep(1)
@@ -0,0 +1,243 @@
1
+ import numpy as np
2
+ from pathlib import Path
3
+
4
+
5
+ def make_lifetime_image(
6
+ canvas,
7
+ output_dir,
8
+ roi_name,
9
+ tau_min_ns=0.0,
10
+ tau_max_ns=5.0,
11
+ smooth_sigma_px=0.0,
12
+ intensity_percentile_lo=0.0,
13
+ intensity_percentile_hi=99.0,
14
+ gamma=0.4,
15
+ dpi=200,
16
+ verbose=True,
17
+ tau_key='tau_mean_amp',
18
+ ):
19
+ import matplotlib
20
+ import matplotlib.pyplot as plt
21
+ import matplotlib.colors as mcolors
22
+ from scipy.ndimage import gaussian_filter
23
+
24
+ # Use non-interactive 'Agg' backend for thread-safe file saving
25
+ # This prevents segfaults when matplotlib operations happen in worker threads
26
+ current_backend = matplotlib.get_backend()
27
+ try:
28
+ matplotlib.use('Agg', force=True)
29
+ except Exception:
30
+ pass
31
+
32
+ try:
33
+ import tifffile as _tifffile
34
+ _has_tifffile = True
35
+ except ImportError:
36
+ _has_tifffile = False
37
+
38
+ from flimkit.configs import FLIM_CMAP
39
+
40
+ output_dir = Path(output_dir)
41
+ output_dir.mkdir(parents=True, exist_ok=True)
42
+
43
+ tau_map = np.asarray(canvas[tau_key], dtype=float)
44
+ int_map = np.asarray(canvas['intensity'], dtype=float)
45
+ _tau_label = {'tau_mean_amp': 'τ_amp', 'tau_mean_int': 'τ_int'}.get(tau_key, 'τ')
46
+ valid = np.isfinite(tau_map) & (int_map > 0)
47
+
48
+ if not valid.any():
49
+ # photon-starved: bail before empty-array reductions crash, but still write blank files
50
+ if verbose:
51
+ print(' [!] No valid pixels (photon-starved) - lifetime image is empty. '
52
+ 'Increase binning so pixels clear min_photons.')
53
+ blank = np.zeros(tau_map.shape, dtype=np.float32)
54
+ if _has_tifffile:
55
+ _tifffile.imwrite(str(output_dir / f"{roi_name}_tau_intensity_weighted.tif"),
56
+ blank.astype(np.uint16))
57
+ _tifffile.imwrite(str(output_dir / f"{roi_name}_tau_intensity_weighted_fullrange.tif"),
58
+ np.full(tau_map.shape, np.nan, dtype=np.float32))
59
+ png_path = output_dir / f"{roi_name}_tau_intensity_weighted.png"
60
+ plt.imsave(str(png_path), np.zeros((*tau_map.shape, 3), dtype=float))
61
+ return png_path
62
+
63
+ if smooth_sigma_px > 0:
64
+ tau_filled = np.where(valid, tau_map, 0.0)
65
+ w = valid.astype(float)
66
+ denom = gaussian_filter(w, sigma=smooth_sigma_px)
67
+ tau_smooth = np.where(denom > 0.01,
68
+ gaussian_filter(tau_filled, sigma=smooth_sigma_px) / denom,
69
+ np.nan)
70
+ else:
71
+ tau_smooth = np.where(valid, tau_map, np.nan)
72
+
73
+ if _has_tifffile:
74
+ # TIF uses unsmoothed tau_map (NaN->0 for unfitted pixels).
75
+ # tau_smooth is only for the PNG - it fills in nearby unfitted
76
+ # pixels via NaN-aware Gaussian, which looks fine in the PNG
77
+ # (multiplied to black by int_norm) but appears as blobs in TIF.
78
+ tau_clipped = np.clip(
79
+ np.where(valid, tau_map, tau_min_ns),
80
+ tau_min_ns, tau_max_ns)
81
+ tau_u16 = ((tau_clipped - tau_min_ns) /
82
+ (tau_max_ns - tau_min_ns + 1e-12) * 65535
83
+ ).astype(np.uint16)
84
+ tiff_path = output_dir / f"{roi_name}_tau_intensity_weighted.tif"
85
+ _tifffile.imwrite(str(tiff_path), tau_u16)
86
+ if verbose:
87
+ print(f" τ TIFF → {tiff_path} "
88
+ f"(uint16, {tau_min_ns}-{tau_max_ns} ns → 0-65535)")
89
+
90
+ # Full-range TIF: also use tau_map so unfitted pixels are NaN, not filled.
91
+ tau_full = np.where(valid, tau_map, np.nan).astype(np.float32)
92
+ tiff_full_path = output_dir / f"{roi_name}_tau_intensity_weighted_fullrange.tif"
93
+ _tifffile.imwrite(str(tiff_full_path), tau_full)
94
+ if verbose:
95
+ tau_vals = tau_map[valid]
96
+ tau_fmin = float(np.nanmin(tau_vals))
97
+ tau_fmax = float(np.nanmax(tau_vals))
98
+ print(f" τ full-range TIFF → {tiff_full_path} "
99
+ f"(float32, {tau_fmin:.3f}-{tau_fmax:.3f} ns, unscaled)")
100
+
101
+ tau_norm = np.clip(
102
+ (tau_smooth - tau_min_ns) / (tau_max_ns - tau_min_ns + 1e-12),
103
+ 0.0, 1.0)
104
+
105
+ int_vals = int_map[valid]
106
+ # lo=0 - never clip the low end; dim tissue stays dim but visible.
107
+ # Clipping lo to a non-zero percentile turns the dimmest real tissue black.
108
+ lo = 0.0
109
+ hi = float(np.percentile(int_vals, intensity_percentile_hi))
110
+ hi = max(hi, 1e-6)
111
+
112
+ # Hard-clip to hi only - removes outlier bright pixels (tile edges, cell
113
+ # clusters) that would otherwise compress bulk tissue toward zero.
114
+ int_clipped = np.clip(int_map, 0.0, hi)
115
+ int_norm = np.power(
116
+ np.clip(int_clipped / hi, 0.0, 1.0), gamma)
117
+ int_norm[~valid] = 0.0
118
+
119
+ if verbose:
120
+ print(f" intensity lo=0 hi={hi:.1f} "
121
+ f"median={np.median(int_vals):.1f} gamma={gamma}")
122
+
123
+ rgb = FLIM_CMAP(tau_norm)[..., :3]
124
+ rgb *= int_norm[..., np.newaxis]
125
+ rgb[~valid] = 0.0
126
+ rgb = np.clip(rgb, 0.0, 1.0)
127
+
128
+ png_path = output_dir / f"{roi_name}_tau_intensity_weighted.png"
129
+ plt.imsave(str(png_path), rgb, dpi=dpi)
130
+ if verbose:
131
+ print(f" lifetime PNG → {png_path}")
132
+
133
+ fig, (ax, cax) = plt.subplots(
134
+ 1, 2, figsize=(14, 6),
135
+ gridspec_kw={'width_ratios': [1, 0.03]})
136
+ ax.imshow(rgb, interpolation='nearest', aspect='equal')
137
+ ax.set_title(
138
+ f"{roi_name} {_tau_label} {tau_min_ns}-{tau_max_ns} ns "
139
+ f"σ={smooth_sigma_px} px γ={gamma}",
140
+ fontsize=10, color='black')
141
+ ax.axis('off')
142
+ ax.set_facecolor('white')
143
+ fig.patch.set_facecolor('white')
144
+
145
+ sm = plt.cm.ScalarMappable(
146
+ cmap=FLIM_CMAP,
147
+ norm=mcolors.Normalize(tau_min_ns, tau_max_ns))
148
+ sm.set_array([])
149
+ cb = fig.colorbar(sm, cax=cax)
150
+ cb.set_label(f'{_tau_label} (ns)', fontsize=10, color='black')
151
+ plt.setp(cb.ax.yaxis.get_ticklabels(), color='black')
152
+ plt.tight_layout(pad=0.3)
153
+
154
+ preview_path = output_dir / f"{roi_name}_tau_intensity_weighted_preview.png"
155
+ plt.savefig(str(preview_path), dpi=150,
156
+ bbox_inches='tight', facecolor='white')
157
+ plt.close(fig)
158
+ if verbose:
159
+ print(f" preview PNG → {preview_path}")
160
+
161
+ if verbose and valid.any():
162
+ tau_v = tau_map[valid]
163
+ print(f" τ_amp median={np.nanmedian(tau_v):.3f} "
164
+ f"mean={np.nanmean(tau_v):.3f} "
165
+ f"p5={np.nanpercentile(tau_v, 5):.3f} "
166
+ f"p95={np.nanpercentile(tau_v, 95):.3f} ns "
167
+ f"n={valid.sum():,}")
168
+
169
+ return png_path
170
+
171
+ def make_component_rgb_tiff(
172
+ canvas,
173
+ output_dir,
174
+ roi_name,
175
+ n_exp,
176
+ intensity_percentile_hi=99.0,
177
+ verbose=True,
178
+ ):
179
+ try:
180
+ import tifffile as _tifffile
181
+ except ImportError:
182
+ raise ImportError('tifffile is required - pip install tifffile')
183
+
184
+ output_dir = Path(output_dir)
185
+ output_dir.mkdir(parents=True, exist_ok=True)
186
+
187
+ intensity = np.asarray(canvas['intensity'], dtype=float)
188
+ H, W = intensity.shape
189
+ channels = []
190
+
191
+ for k in range(1, n_exp + 1):
192
+ amp_key = f'a{k}'
193
+ amp = canvas.get(amp_key)
194
+ if amp is None:
195
+ channels.append(np.zeros((H, W), dtype=np.uint16))
196
+ continue
197
+
198
+ amp = np.asarray(amp, dtype=float)
199
+ fitted = np.isfinite(amp) & (intensity > 0)
200
+
201
+ # Express as amplitude fraction (composition) to remove intensity bias
202
+ total_amp = np.zeros((H, W), dtype=float)
203
+ for j in range(1, n_exp + 1):
204
+ a_j = canvas.get(f'a{j}')
205
+ if a_j is not None:
206
+ total_amp += np.where(np.isfinite(a_j), a_j, 0.0)
207
+
208
+ with np.errstate(invalid='ignore', divide='ignore'):
209
+ frac = np.where((fitted) & (total_amp > 0), amp / total_amp, np.nan)
210
+
211
+ # Scale to uint16 - clip outliers at hi percentile
212
+ vals = frac[np.isfinite(frac)]
213
+ if vals.size > 0:
214
+ hi = float(np.percentile(vals, intensity_percentile_hi))
215
+ hi = max(hi, 1e-9)
216
+ ch_u16 = np.where(
217
+ np.isfinite(frac),
218
+ np.clip(frac / hi, 0.0, 1.0) * 65535,
219
+ 0.0
220
+ ).astype(np.uint16)
221
+ else:
222
+ ch_u16 = np.zeros((H, W), dtype=np.uint16)
223
+
224
+ channels.append(ch_u16)
225
+
226
+ # Pad to 3 channels (RGB) with zeros for unused channels
227
+ while len(channels) < 3:
228
+ channels.append(np.zeros((H, W), dtype=np.uint16))
229
+
230
+ # Stack as (H, W, 3) RGB
231
+ rgb_u16 = np.stack(channels[:3], axis=-1)
232
+
233
+ labels = ['R=τ₁', 'G=τ₂', 'B=τ₃']
234
+ channel_info = ' '.join(labels[:n_exp])
235
+
236
+ tiff_path = output_dir / f"{roi_name}_component_rgb.tif"
237
+ _tifffile.imwrite(str(tiff_path), rgb_u16, photometric='rgb')
238
+
239
+ if verbose:
240
+ print(f" component RGB TIFF → {tiff_path}")
241
+ print(f" {channel_info} (amplitude fraction, uint16)")
242
+
243
+ return tiff_path