flimkit 0.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flimkit/FLIM/__init__.py +0 -0
- flimkit/FLIM/assemble.py +254 -0
- flimkit/FLIM/batch.py +681 -0
- flimkit/FLIM/bg_tools.py +51 -0
- flimkit/FLIM/fit_tools.py +244 -0
- flimkit/FLIM/fitters.py +1471 -0
- flimkit/FLIM/irf_tools.py +617 -0
- flimkit/FLIM/models.py +391 -0
- flimkit/GPU/__init__.py +85 -0
- flimkit/GPU/_base.py +391 -0
- flimkit/GPU/cuda.py +10 -0
- flimkit/GPU/mlx_backend.py +381 -0
- flimkit/GPU/mps.py +10 -0
- flimkit/GPU/rocm.py +10 -0
- flimkit/GPU/torch_backend.py +385 -0
- flimkit/UI/app_state.py +10 -0
- flimkit/UI/controller.py +139 -0
- flimkit/UI/expert_settings.py +248 -0
- flimkit/UI/fit_help.py +206 -0
- flimkit/UI/fov_preview.py +1085 -0
- flimkit/UI/gui.py +3919 -0
- flimkit/UI/icon.icns +0 -0
- flimkit/UI/icon.ico +0 -0
- flimkit/UI/icon.png +0 -0
- flimkit/UI/irf_widget.py +103 -0
- flimkit/UI/mode_controller.py +118 -0
- flimkit/UI/modes/__init__.py +0 -0
- flimkit/UI/modes/base.py +3 -0
- flimkit/UI/modes/batch_mode.py +312 -0
- flimkit/UI/modes/fov_mode.py +164 -0
- flimkit/UI/modes/irf_mode.py +80 -0
- flimkit/UI/modes/phasor_mode.py +131 -0
- flimkit/UI/modes/stitch_mode.py +254 -0
- flimkit/UI/phasor_panel.py +1087 -0
- flimkit/UI/progress_window.py +113 -0
- flimkit/UI/project_panel.py +262 -0
- flimkit/UI/results_panel.py +332 -0
- flimkit/UI/roi_tools.py +794 -0
- flimkit/UI/utils.py +217 -0
- flimkit/__init__.py +0 -0
- flimkit/_version.py +41 -0
- flimkit/cli.py +120 -0
- flimkit/configs.py +148 -0
- flimkit/dialogs.py +46 -0
- flimkit/formats/BH/__init__.py +0 -0
- flimkit/formats/BH/reader.py +296 -0
- flimkit/formats/BH/writer.py +86 -0
- flimkit/formats/ISS/__init__.py +0 -0
- flimkit/formats/ISS/fdflim.py +86 -0
- flimkit/formats/ISS/image.py +114 -0
- flimkit/formats/ISS/reader.py +223 -0
- flimkit/formats/PS/__init__.py +0 -0
- flimkit/formats/PS/reader.py +202 -0
- flimkit/formats/PTU/__init__.py +0 -0
- flimkit/formats/PTU/decode.py +27 -0
- flimkit/formats/PTU/phu.py +85 -0
- flimkit/formats/PTU/reader.py +235 -0
- flimkit/formats/PTU/series.py +258 -0
- flimkit/formats/PTU/stitch.py +1182 -0
- flimkit/formats/PTU/tools.py +94 -0
- flimkit/formats/__init__.py +2 -0
- flimkit/formats/flim_file.py +232 -0
- flimkit/formats/phasor.py +132 -0
- flimkit/formats/signal.py +170 -0
- flimkit/image/tools.py +124 -0
- flimkit/interactive.py +1857 -0
- flimkit/mpl_backend.py +22 -0
- flimkit/phasor/__init__.py +40 -0
- flimkit/phasor/filters.py +127 -0
- flimkit/phasor/fret.py +654 -0
- flimkit/phasor/interactive.py +556 -0
- flimkit/phasor/peaks.py +186 -0
- flimkit/phasor/signal.py +90 -0
- flimkit/phasor_launcher.py +314 -0
- flimkit/plugins/__init__.py +137 -0
- flimkit/plugins/bindings.py +116 -0
- flimkit/plugins/builtin/__init__.py +3 -0
- flimkit/plugins/builtin/core_tools.py +28 -0
- flimkit/plugins/loader.py +371 -0
- flimkit/plugins/registry.py +406 -0
- flimkit/project.py +197 -0
- flimkit/synth.py +145 -0
- flimkit/utils/__init__.py +0 -0
- flimkit/utils/batch_fit.py +301 -0
- flimkit/utils/config_manager.py +119 -0
- flimkit/utils/config_snapshot.py +30 -0
- flimkit/utils/crash_handler.py +183 -0
- flimkit/utils/display.py +197 -0
- flimkit/utils/enhanced_outputs.py +345 -0
- flimkit/utils/fancy.py +103 -0
- flimkit/utils/lifetime_image.py +243 -0
- flimkit/utils/misc.py +111 -0
- flimkit/utils/plotting.py +190 -0
- flimkit/utils/roi.py +370 -0
- flimkit/utils/session.py +51 -0
- flimkit/utils/update_check.py +198 -0
- flimkit/utils/xlsx_tools.py +97 -0
- flimkit/utils/xml_utils.py +219 -0
- flimkit-0.12.0.dist-info/METADATA +356 -0
- flimkit-0.12.0.dist-info/RECORD +104 -0
- flimkit-0.12.0.dist-info/WHEEL +5 -0
- flimkit-0.12.0.dist-info/entry_points.txt +2 -0
- flimkit-0.12.0.dist-info/licenses/LICENSE.md +11 -0
- flimkit-0.12.0.dist-info/top_level.txt +1 -0
|
@@ -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
|