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
File without changes
@@ -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