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
flimkit/FLIM/__init__.py
ADDED
|
File without changes
|
flimkit/FLIM/assemble.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List, Dict, Any, Optional
|
|
4
|
+
import tifffile
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Cancelled(Exception):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
def assemble_tile_maps(
|
|
11
|
+
tile_results: List[Dict[str, Any]],
|
|
12
|
+
canvas_height: int,
|
|
13
|
+
canvas_width: int,
|
|
14
|
+
n_exp: int,
|
|
15
|
+
cancel_event=None,
|
|
16
|
+
) -> Dict[str, np.ndarray]:
|
|
17
|
+
H, W = canvas_height, canvas_width
|
|
18
|
+
|
|
19
|
+
keys_scalar = ['tau_mean_amp', 'tau_mean_int', 'chi2', 'calibrated_chi2_r']
|
|
20
|
+
keys_tau = [f'tau{k}' for k in range(1, n_exp + 1)]
|
|
21
|
+
keys_amp = [f'a{k}' for k in range(1, n_exp + 1)]
|
|
22
|
+
keys_all = keys_scalar + keys_tau + keys_amp
|
|
23
|
+
|
|
24
|
+
# Build nearest-centre ownership map
|
|
25
|
+
# For every canvas pixel, record the index of the tile whose centre is
|
|
26
|
+
# closest. Ties broken by later tiles (last write wins, doesn't matter).
|
|
27
|
+
# Use squared Euclidean distance - no sqrt needed for comparison.
|
|
28
|
+
min_dist2 = np.full((H, W), np.inf, dtype=np.float64)
|
|
29
|
+
owner = np.full((H, W), -1, dtype=np.int32)
|
|
30
|
+
|
|
31
|
+
for ti, tr in enumerate(tile_results):
|
|
32
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
33
|
+
raise Cancelled('cancelled while mapping tile ownership')
|
|
34
|
+
if tr.get('pixel_maps') is None:
|
|
35
|
+
continue
|
|
36
|
+
y0, x0 = tr['pixel_y'], tr['pixel_x']
|
|
37
|
+
th, tw = tr['tile_h'], tr['tile_w']
|
|
38
|
+
y1 = min(y0 + th, H)
|
|
39
|
+
x1 = min(x0 + tw, W)
|
|
40
|
+
|
|
41
|
+
cy = y0 + th / 2.0
|
|
42
|
+
cx = x0 + tw / 2.0
|
|
43
|
+
|
|
44
|
+
rows = np.arange(y0, y1, dtype=np.float64)
|
|
45
|
+
cols = np.arange(x0, x1, dtype=np.float64)
|
|
46
|
+
dy2 = (rows - cy) ** 2
|
|
47
|
+
dx2 = (cols - cx) ** 2
|
|
48
|
+
dist2 = dy2[:, np.newaxis] + dx2
|
|
49
|
+
|
|
50
|
+
# Only take ownership where this tile is strictly closer
|
|
51
|
+
region = min_dist2[y0:y1, x0:x1]
|
|
52
|
+
closer = dist2 < region
|
|
53
|
+
min_dist2[y0:y1, x0:x1] = np.where(closer, dist2, region)
|
|
54
|
+
owner[y0:y1, x0:x1] = np.where(closer, ti, owner[y0:y1, x0:x1])
|
|
55
|
+
|
|
56
|
+
canvas = {k: np.full((H, W), np.nan, dtype=np.float32) for k in keys_all}
|
|
57
|
+
intensity_canvas = np.zeros((H, W), dtype=np.float32)
|
|
58
|
+
coverage = np.zeros((H, W), dtype=np.uint16)
|
|
59
|
+
|
|
60
|
+
for ti, tr in enumerate(tile_results):
|
|
61
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
62
|
+
raise Cancelled('cancelled while assembling the canvas')
|
|
63
|
+
pm = tr.get('pixel_maps')
|
|
64
|
+
if pm is None:
|
|
65
|
+
continue
|
|
66
|
+
y0, x0 = tr['pixel_y'], tr['pixel_x']
|
|
67
|
+
th, tw = tr['tile_h'], tr['tile_w']
|
|
68
|
+
y1 = min(y0 + th, H)
|
|
69
|
+
x1 = min(x0 + tw, W)
|
|
70
|
+
dy, dx = y1 - y0, x1 - x0
|
|
71
|
+
|
|
72
|
+
owned = (owner[y0:y1, x0:x1] == ti)
|
|
73
|
+
|
|
74
|
+
tile_int = np.asarray(
|
|
75
|
+
pm.get('intensity', np.zeros((th, tw), dtype=np.float32)),
|
|
76
|
+
dtype=np.float32)[:dy, :dx]
|
|
77
|
+
|
|
78
|
+
intensity_canvas[y0:y1, x0:x1] = np.where(
|
|
79
|
+
owned, tile_int, intensity_canvas[y0:y1, x0:x1])
|
|
80
|
+
coverage[y0:y1, x0:x1] = np.where(
|
|
81
|
+
owned, coverage[y0:y1, x0:x1] + 1, coverage[y0:y1, x0:x1])
|
|
82
|
+
|
|
83
|
+
for key in keys_all:
|
|
84
|
+
raw = pm.get(key)
|
|
85
|
+
if raw is None:
|
|
86
|
+
continue
|
|
87
|
+
raw = np.asarray(raw, dtype=np.float32)[:dy, :dx]
|
|
88
|
+
# Only place fitted (finite) pixels that this tile owns
|
|
89
|
+
place = owned & np.isfinite(raw)
|
|
90
|
+
canvas[key][y0:y1, x0:x1] = np.where(
|
|
91
|
+
place, raw, canvas[key][y0:y1, x0:x1])
|
|
92
|
+
|
|
93
|
+
canvas['intensity'] = intensity_canvas
|
|
94
|
+
canvas['coverage'] = coverage.astype(np.float32)
|
|
95
|
+
|
|
96
|
+
n_covered = int((owner >= 0).sum())
|
|
97
|
+
print(
|
|
98
|
+
f" Canvas {H}×{W} | tiles={len(tile_results)} "
|
|
99
|
+
f"| covered={n_covered:,} px "
|
|
100
|
+
f"| ownership map built (nearest-centre, no blending)"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return canvas
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def derive_global_tau(
|
|
107
|
+
canvas: Dict[str, np.ndarray],
|
|
108
|
+
n_exp: int,
|
|
109
|
+
tissue_mask: Optional[np.ndarray] = None,
|
|
110
|
+
) -> Dict[str, Any]:
|
|
111
|
+
tau_arrays = [canvas[f'tau{k}'] for k in range(1, n_exp + 1)]
|
|
112
|
+
a_arrays = [canvas[f'a{k}'] for k in range(1, n_exp + 1)]
|
|
113
|
+
|
|
114
|
+
total_amp = np.zeros_like(tau_arrays[0])
|
|
115
|
+
tau_mean = np.zeros_like(tau_arrays[0])
|
|
116
|
+
for tau_k, a_k in zip(tau_arrays, a_arrays):
|
|
117
|
+
valid = np.isfinite(tau_k) & np.isfinite(a_k)
|
|
118
|
+
tau_mean = np.where(valid, tau_mean + tau_k * a_k, tau_mean)
|
|
119
|
+
total_amp = np.where(valid, total_amp + a_k, total_amp)
|
|
120
|
+
|
|
121
|
+
with np.errstate(invalid='ignore', divide='ignore'):
|
|
122
|
+
tau_mean_amp = np.where(total_amp > 0, tau_mean / total_amp, np.nan)
|
|
123
|
+
|
|
124
|
+
valid_px = np.isfinite(tau_mean_amp)
|
|
125
|
+
if tissue_mask is not None:
|
|
126
|
+
valid_px = valid_px & tissue_mask
|
|
127
|
+
|
|
128
|
+
if not valid_px.any():
|
|
129
|
+
return {'error': 'No valid fitted pixels found'}
|
|
130
|
+
|
|
131
|
+
summary = {
|
|
132
|
+
'n_pixels_fitted': int(valid_px.sum()),
|
|
133
|
+
'tau_mean_amp_global_ns': float(np.nanmean(tau_mean_amp[valid_px])),
|
|
134
|
+
'tau_std_amp_global_ns': float(np.nanstd(tau_mean_amp[valid_px])),
|
|
135
|
+
'tau_median_amp_global_ns': float(np.nanmedian(tau_mean_amp[valid_px])),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for k, (tau_k, a_k) in enumerate(zip(tau_arrays, a_arrays), start=1):
|
|
139
|
+
valid_k = np.isfinite(tau_k) & np.isfinite(a_k) & valid_px
|
|
140
|
+
if valid_k.any():
|
|
141
|
+
summary[f'tau{k}_mean_ns'] = float(
|
|
142
|
+
np.average(tau_k[valid_k], weights=a_k[valid_k]))
|
|
143
|
+
summary[f'tau{k}_std_ns'] = float(np.nanstd(tau_k[valid_k]))
|
|
144
|
+
summary[f'a{k}_mean_frac'] = float(
|
|
145
|
+
np.nanmean(a_k[valid_k] / total_amp[valid_k]))
|
|
146
|
+
|
|
147
|
+
return summary
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def save_assembled_maps(
|
|
151
|
+
canvas: Dict[str, np.ndarray],
|
|
152
|
+
global_summary: Dict[str, Any],
|
|
153
|
+
output_dir: Path,
|
|
154
|
+
roi_name: str,
|
|
155
|
+
n_exp: int,
|
|
156
|
+
tau_display_min: Optional[float] = None,
|
|
157
|
+
tau_display_max: Optional[float] = None,
|
|
158
|
+
intensity_display_min: Optional[float] = None,
|
|
159
|
+
intensity_display_max: Optional[float] = None,
|
|
160
|
+
tau_weighting: str = 'amp',
|
|
161
|
+
):
|
|
162
|
+
output_dir = Path(output_dir)
|
|
163
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
|
|
165
|
+
# Raw .npy maps are saved for every canvas key (both tau_mean_amp and
|
|
166
|
+
# tau_mean_int), unclipped. tau_weighting only selects which one becomes
|
|
167
|
+
# the display tau TIFF below.
|
|
168
|
+
for key, arr in canvas.items():
|
|
169
|
+
np.save(str(output_dir / f"{roi_name}_{key}.npy"), arr)
|
|
170
|
+
|
|
171
|
+
tau_key = 'tau_mean_int' if tau_weighting == 'int' else 'tau_mean_amp'
|
|
172
|
+
if tau_key not in canvas:
|
|
173
|
+
tau_key = 'tau_mean_amp'
|
|
174
|
+
|
|
175
|
+
# Use percentile-based clipping so a handful of very bright pixels (cell
|
|
176
|
+
# clusters, tile-overlap edges) do not compress the bulk of tissue to
|
|
177
|
+
# near-zero. Display range overrides take priority when provided.
|
|
178
|
+
intensity = canvas['intensity']
|
|
179
|
+
pos = intensity[intensity > 0]
|
|
180
|
+
if pos.size > 0:
|
|
181
|
+
# lo=0 so dim tissue is never clipped to black.
|
|
182
|
+
# hi=99.5th percentile clips only the top 0.5% of bright outliers.
|
|
183
|
+
i_min = float(intensity_display_min) if intensity_display_min is not None else 0.0
|
|
184
|
+
i_max = float(intensity_display_max) if intensity_display_max is not None else float(np.percentile(pos, 99.0))
|
|
185
|
+
i_max = max(i_max, i_min + 1e-6)
|
|
186
|
+
intensity_scaled = np.clip(
|
|
187
|
+
np.clip(intensity, 0.0, i_max) / i_max * 65535,
|
|
188
|
+
0, 65535
|
|
189
|
+
).astype(np.uint16)
|
|
190
|
+
else:
|
|
191
|
+
intensity_scaled = np.zeros_like(intensity, dtype=np.uint16)
|
|
192
|
+
i_min, i_max = 0.0, 0.0
|
|
193
|
+
tifffile.imwrite(str(output_dir / f"{roi_name}_intensity.tif"), intensity_scaled)
|
|
194
|
+
print(f" intensity display range: {i_min:.1f} - {i_max:.1f} counts")
|
|
195
|
+
|
|
196
|
+
# tau_mean_amp TIFF (uint16)
|
|
197
|
+
# 0 → tau_min (or 0 if auto)
|
|
198
|
+
# 65535 → tau_max (or nanmax if auto)
|
|
199
|
+
# 0 → unfitted pixels (NaN → 0 before cast)
|
|
200
|
+
# This matches the FLIM microscope export convention.
|
|
201
|
+
tau_map = canvas[tau_key]
|
|
202
|
+
finite = np.isfinite(tau_map)
|
|
203
|
+
|
|
204
|
+
t_min = float(tau_display_min) if tau_display_min is not None \
|
|
205
|
+
else float(np.nanpercentile(tau_map[finite], 0.5) if finite.any() else 0.0)
|
|
206
|
+
t_max = float(tau_display_max) if tau_display_max is not None \
|
|
207
|
+
else float(np.nanpercentile(tau_map[finite], 99.5) if finite.any() else 1.0)
|
|
208
|
+
t_max = max(t_max, t_min + 1e-6)
|
|
209
|
+
|
|
210
|
+
tau_u16 = np.where(
|
|
211
|
+
finite,
|
|
212
|
+
np.clip((tau_map - t_min) / (t_max - t_min) * 65535, 0, 65535),
|
|
213
|
+
0
|
|
214
|
+
).astype(np.uint16)
|
|
215
|
+
tifffile.imwrite(str(output_dir / f"{roi_name}_{tau_key}.tif"), tau_u16)
|
|
216
|
+
print(f" {tau_key} display range: {t_min:.3f} - {t_max:.3f} ns "
|
|
217
|
+
f"→ 0 - 65535")
|
|
218
|
+
|
|
219
|
+
# Global summary text
|
|
220
|
+
summary_path = output_dir / f"{roi_name}_global_summary.txt"
|
|
221
|
+
with open(summary_path, 'w') as f:
|
|
222
|
+
f.write('=' * 60 + '\n')
|
|
223
|
+
f.write('PER-TILE FIT - GLOBAL TAU SUMMARY\n')
|
|
224
|
+
f.write('=' * 60 + '\n\n')
|
|
225
|
+
f.write(f"ROI: {roi_name}\n")
|
|
226
|
+
n_pixels_fitted = global_summary.get('n_pixels_fitted', 'N/A')
|
|
227
|
+
if isinstance(n_pixels_fitted, int):
|
|
228
|
+
f.write(f"Pixels fitted: {n_pixels_fitted:,}\n\n")
|
|
229
|
+
else:
|
|
230
|
+
f.write(f"Pixels fitted: {n_pixels_fitted}\n\n")
|
|
231
|
+
f.write('Amplitude-weighted mean lifetime (global):\n')
|
|
232
|
+
f.write(f" tau_mean = {global_summary.get('tau_mean_amp_global_ns', float('nan')):.4f} ns\n")
|
|
233
|
+
f.write(f" tau_std = {global_summary.get('tau_std_amp_global_ns', float('nan')):.4f} ns\n")
|
|
234
|
+
f.write(f" tau_median= {global_summary.get('tau_median_amp_global_ns', float('nan')):.4f} ns\n\n")
|
|
235
|
+
f.write('Per-component (amplitude-weighted across ROI):\n')
|
|
236
|
+
for k in range(1, n_exp + 1):
|
|
237
|
+
tau_k = global_summary.get(f'tau{k}_mean_ns', None)
|
|
238
|
+
a_k = global_summary.get(f'a{k}_mean_frac', None)
|
|
239
|
+
if tau_k is not None:
|
|
240
|
+
f.write(f" tau{k} = {tau_k:.4f} ns "
|
|
241
|
+
f"a{k} = {a_k:.3f} (mean amplitude fraction)\n")
|
|
242
|
+
f.write('\n' + '=' * 60 + '\n')
|
|
243
|
+
|
|
244
|
+
# Component RGB TIFF
|
|
245
|
+
try:
|
|
246
|
+
from ..utils.lifetime_image import make_component_rgb_tiff
|
|
247
|
+
make_component_rgb_tiff(canvas, output_dir, roi_name, n_exp, verbose=True)
|
|
248
|
+
except Exception as _e:
|
|
249
|
+
print(f" component RGB skipped: {_e}")
|
|
250
|
+
|
|
251
|
+
print(f" Assembled maps saved to {output_dir}")
|
|
252
|
+
print(f" Global summary: {summary_path.name}")
|
|
253
|
+
|
|
254
|
+
return summary_path
|