flimkit 0.13.7__tar.gz → 0.13.8__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 (112) hide show
  1. {flimkit-0.13.7 → flimkit-0.13.8}/PKG-INFO +1 -1
  2. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/FLIM/assemble.py +44 -98
  3. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/FLIM/batch.py +15 -4
  4. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/FLIM/irf_tools.py +78 -65
  5. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/fov_preview.py +97 -99
  6. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/gui.py +59 -47
  7. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/_version.py +3 -2
  8. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/PTU/stitch.py +70 -55
  9. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/interactive.py +15 -9
  10. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/project.py +17 -1
  11. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/utils/display.py +58 -3
  12. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/utils/enhanced_outputs.py +55 -122
  13. flimkit-0.13.8/flimkit/utils/export_png.py +83 -0
  14. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/utils/lifetime_image.py +37 -85
  15. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit.egg-info/PKG-INFO +1 -1
  16. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit.egg-info/SOURCES.txt +1 -0
  17. {flimkit-0.13.7 → flimkit-0.13.8}/LICENSE.md +0 -0
  18. {flimkit-0.13.7 → flimkit-0.13.8}/README.md +0 -0
  19. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/FLIM/__init__.py +0 -0
  20. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/FLIM/bg_tools.py +0 -0
  21. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/FLIM/fit_tools.py +0 -0
  22. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/FLIM/fitters.py +0 -0
  23. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/FLIM/models.py +0 -0
  24. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/GPU/__init__.py +0 -0
  25. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/GPU/_base.py +0 -0
  26. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/GPU/cuda.py +0 -0
  27. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/GPU/mlx_backend.py +0 -0
  28. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/GPU/mps.py +0 -0
  29. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/GPU/rocm.py +0 -0
  30. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/GPU/torch_backend.py +0 -0
  31. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/app_state.py +0 -0
  32. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/controller.py +0 -0
  33. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/expert_settings.py +0 -0
  34. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/fit_help.py +0 -0
  35. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/icon.icns +0 -0
  36. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/icon.ico +0 -0
  37. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/icon.png +0 -0
  38. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/irf_widget.py +0 -0
  39. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/mode_controller.py +0 -0
  40. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/modes/__init__.py +0 -0
  41. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/modes/base.py +0 -0
  42. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/modes/batch_mode.py +0 -0
  43. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/modes/fov_mode.py +0 -0
  44. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/modes/irf_mode.py +0 -0
  45. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/modes/phasor_mode.py +0 -0
  46. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/modes/stitch_mode.py +0 -0
  47. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/phasor_panel.py +0 -0
  48. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/progress_window.py +0 -0
  49. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/project_panel.py +0 -0
  50. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/results_panel.py +0 -0
  51. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/roi_tools.py +0 -0
  52. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/UI/utils.py +0 -0
  53. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/__init__.py +0 -0
  54. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/cli.py +0 -0
  55. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/configs.py +0 -0
  56. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/dialogs.py +0 -0
  57. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/BH/__init__.py +0 -0
  58. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/BH/reader.py +0 -0
  59. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/BH/writer.py +0 -0
  60. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/ISS/__init__.py +0 -0
  61. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/ISS/fdflim.py +0 -0
  62. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/ISS/image.py +0 -0
  63. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/ISS/reader.py +0 -0
  64. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/PS/__init__.py +0 -0
  65. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/PS/reader.py +0 -0
  66. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/PTU/__init__.py +0 -0
  67. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/PTU/decode.py +0 -0
  68. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/PTU/phu.py +0 -0
  69. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/PTU/reader.py +0 -0
  70. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/PTU/series.py +0 -0
  71. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/PTU/tools.py +0 -0
  72. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/__init__.py +0 -0
  73. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/flim_file.py +0 -0
  74. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/phasor.py +0 -0
  75. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/formats/signal.py +0 -0
  76. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/image/tools.py +0 -0
  77. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/mpl_backend.py +0 -0
  78. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/phasor/__init__.py +0 -0
  79. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/phasor/filters.py +0 -0
  80. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/phasor/fret.py +0 -0
  81. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/phasor/interactive.py +0 -0
  82. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/phasor/peaks.py +0 -0
  83. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/phasor/signal.py +0 -0
  84. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/phasor_launcher.py +0 -0
  85. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/plugins/__init__.py +0 -0
  86. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/plugins/bindings.py +0 -0
  87. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/plugins/builtin/__init__.py +0 -0
  88. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/plugins/builtin/core_tools.py +0 -0
  89. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/plugins/loader.py +0 -0
  90. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/plugins/registry.py +0 -0
  91. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/synth.py +0 -0
  92. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/utils/__init__.py +0 -0
  93. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/utils/apply_settings.py +0 -0
  94. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/utils/batch_fit.py +0 -0
  95. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/utils/config_manager.py +0 -0
  96. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/utils/config_snapshot.py +0 -0
  97. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/utils/crash_handler.py +0 -0
  98. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/utils/fancy.py +0 -0
  99. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/utils/misc.py +0 -0
  100. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/utils/ome_zarr.py +0 -0
  101. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/utils/plotting.py +0 -0
  102. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/utils/roi.py +0 -0
  103. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/utils/session.py +0 -0
  104. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/utils/update_check.py +0 -0
  105. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/utils/xlsx_tools.py +0 -0
  106. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit/utils/xml_utils.py +0 -0
  107. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit.egg-info/dependency_links.txt +0 -0
  108. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit.egg-info/entry_points.txt +0 -0
  109. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit.egg-info/requires.txt +0 -0
  110. {flimkit-0.13.7 → flimkit-0.13.8}/flimkit.egg-info/top_level.txt +0 -0
  111. {flimkit-0.13.7 → flimkit-0.13.8}/pyproject.toml +0 -0
  112. {flimkit-0.13.7 → flimkit-0.13.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flimkit
3
- Version: 0.13.7
3
+ Version: 0.13.8
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
@@ -2,7 +2,7 @@ import numpy as np
2
2
  from pathlib import Path
3
3
  from typing import List, Dict, Any, Optional
4
4
  import tifffile
5
-
5
+ from flimkit.utils.export_png import tiff_resolution
6
6
 
7
7
  class Cancelled(Exception):
8
8
  pass
@@ -15,48 +15,35 @@ def assemble_tile_maps(
15
15
  cancel_event=None,
16
16
  ) -> Dict[str, np.ndarray]:
17
17
  H, W = canvas_height, canvas_width
18
-
19
18
  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.
19
+ keys_tau = [f'tau{k}' for k in range(1, n_exp + 1)]
20
+ keys_amp = [f'a{k}' for k in range(1, n_exp + 1)]
21
+ keys_all = keys_scalar + keys_tau + keys_amp
28
22
  min_dist2 = np.full((H, W), np.inf, dtype=np.float64)
29
- owner = np.full((H, W), -1, dtype=np.int32)
30
-
23
+ owner = np.full((H, W), -1, dtype=np.int32)
31
24
  for ti, tr in enumerate(tile_results):
32
25
  if cancel_event is not None and cancel_event.is_set():
33
26
  raise Cancelled('cancelled while mapping tile ownership')
34
27
  if tr.get('pixel_maps') is None:
35
28
  continue
36
29
  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
-
30
+ th, tw = tr['tile_h'], tr['tile_w']
31
+ y1 = min(y0 + th, H)
32
+ x1 = min(x0 + tw, W)
41
33
  cy = y0 + th / 2.0
42
34
  cx = x0 + tw / 2.0
43
-
44
35
  rows = np.arange(y0, y1, dtype=np.float64)
45
36
  cols = np.arange(x0, x1, dtype=np.float64)
46
- dy2 = (rows - cy) ** 2
47
- dx2 = (cols - cx) ** 2
37
+ dy2 = (rows - cy) ** 2
38
+ dx2 = (cols - cx) ** 2
48
39
  dist2 = dy2[:, np.newaxis] + dx2
49
-
50
- # Only take ownership where this tile is strictly closer
51
40
  region = min_dist2[y0:y1, x0:x1]
52
41
  closer = dist2 < region
53
42
  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
-
43
+ owner[y0:y1, x0:x1] = np.where(closer, ti, owner[y0:y1, x0:x1])
56
44
  canvas = {k: np.full((H, W), np.nan, dtype=np.float32) for k in keys_all}
57
45
  intensity_canvas = np.zeros((H, W), dtype=np.float32)
58
- coverage = np.zeros((H, W), dtype=np.uint16)
59
-
46
+ coverage = np.zeros((H, W), dtype=np.uint16)
60
47
  for ti, tr in enumerate(tile_results):
61
48
  if cancel_event is not None and cancel_event.is_set():
62
49
  raise Cancelled('cancelled while assembling the canvas')
@@ -64,89 +51,72 @@ def assemble_tile_maps(
64
51
  if pm is None:
65
52
  continue
66
53
  y0, x0 = tr['pixel_y'], tr['pixel_x']
67
- th, tw = tr['tile_h'], tr['tile_w']
54
+ th, tw = tr['tile_h'], tr['tile_w']
68
55
  y1 = min(y0 + th, H)
69
56
  x1 = min(x0 + tw, W)
70
57
  dy, dx = y1 - y0, x1 - x0
71
-
72
58
  owned = (owner[y0:y1, x0:x1] == ti)
73
-
74
59
  tile_int = np.asarray(
75
60
  pm.get('intensity', np.zeros((th, tw), dtype=np.float32)),
76
61
  dtype=np.float32)[:dy, :dx]
77
-
78
62
  intensity_canvas[y0:y1, x0:x1] = np.where(
79
63
  owned, tile_int, intensity_canvas[y0:y1, x0:x1])
80
64
  coverage[y0:y1, x0:x1] = np.where(
81
65
  owned, coverage[y0:y1, x0:x1] + 1, coverage[y0:y1, x0:x1])
82
-
83
66
  for key in keys_all:
84
67
  raw = pm.get(key)
85
68
  if raw is None:
86
69
  continue
87
70
  raw = np.asarray(raw, dtype=np.float32)[:dy, :dx]
88
- # Only place fitted (finite) pixels that this tile owns
89
71
  place = owned & np.isfinite(raw)
90
72
  canvas[key][y0:y1, x0:x1] = np.where(
91
73
  place, raw, canvas[key][y0:y1, x0:x1])
92
-
93
74
  canvas['intensity'] = intensity_canvas
94
- canvas['coverage'] = coverage.astype(np.float32)
95
-
75
+ canvas['coverage'] = coverage.astype(np.float32)
96
76
  n_covered = int((owner >= 0).sum())
97
77
  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)"
78
+ f' Canvas {H}×{W} | tiles={len(tile_results)} '
79
+ f'| covered={n_covered:,} px '
80
+ f'| ownership map built (nearest-centre, no blending)'
101
81
  )
102
-
103
82
  return canvas
104
83
 
105
-
106
84
  def derive_global_tau(
107
85
  canvas: Dict[str, np.ndarray],
108
86
  n_exp: int,
109
87
  tissue_mask: Optional[np.ndarray] = None,
110
88
  ) -> Dict[str, Any]:
111
89
  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
-
90
+ a_arrays = [canvas[f'a{k}'] for k in range(1, n_exp + 1)]
114
91
  total_amp = np.zeros_like(tau_arrays[0])
115
- tau_mean = np.zeros_like(tau_arrays[0])
92
+ tau_mean = np.zeros_like(tau_arrays[0])
116
93
  for tau_k, a_k in zip(tau_arrays, a_arrays):
117
94
  valid = np.isfinite(tau_k) & np.isfinite(a_k)
118
- tau_mean = np.where(valid, tau_mean + tau_k * a_k, tau_mean)
95
+ tau_mean = np.where(valid, tau_mean + tau_k * a_k, tau_mean)
119
96
  total_amp = np.where(valid, total_amp + a_k, total_amp)
120
-
121
97
  with np.errstate(invalid='ignore', divide='ignore'):
122
98
  tau_mean_amp = np.where(total_amp > 0, tau_mean / total_amp, np.nan)
123
-
124
99
  valid_px = np.isfinite(tau_mean_amp)
125
100
  if tissue_mask is not None:
126
101
  valid_px = valid_px & tissue_mask
127
-
128
102
  if not valid_px.any():
129
103
  return {'error': 'No valid fitted pixels found'}
130
-
131
104
  summary = {
132
105
  'n_pixels_fitted': int(valid_px.sum()),
133
106
  'tau_mean_amp_global_ns': float(np.nanmean(tau_mean_amp[valid_px])),
134
107
  'tau_std_amp_global_ns': float(np.nanstd(tau_mean_amp[valid_px])),
135
108
  'tau_median_amp_global_ns': float(np.nanmedian(tau_mean_amp[valid_px])),
136
109
  }
137
-
138
110
  for k, (tau_k, a_k) in enumerate(zip(tau_arrays, a_arrays), start=1):
139
111
  valid_k = np.isfinite(tau_k) & np.isfinite(a_k) & valid_px
140
112
  if valid_k.any():
141
113
  summary[f'tau{k}_mean_ns'] = float(
142
114
  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]))
115
+ summary[f'tau{k}_std_ns'] = float(np.nanstd(tau_k[valid_k]))
144
116
  summary[f'a{k}_mean_frac'] = float(
145
117
  np.nanmean(a_k[valid_k] / total_amp[valid_k]))
146
-
147
118
  return summary
148
119
 
149
-
150
120
  def save_assembled_maps(
151
121
  canvas: Dict[str, np.ndarray],
152
122
  global_summary: Dict[str, Any],
@@ -158,28 +128,18 @@ def save_assembled_maps(
158
128
  intensity_display_min: Optional[float] = None,
159
129
  intensity_display_max: Optional[float] = None,
160
130
  tau_weighting: str = 'amp',
131
+ field_area_um2: Optional[float] = None,
161
132
  ):
162
133
  output_dir = Path(output_dir)
163
134
  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
135
  for key, arr in canvas.items():
169
- np.save(str(output_dir / f"{roi_name}_{key}.npy"), arr)
170
-
136
+ np.save(str(output_dir / f'{roi_name}_{key}.npy'), arr)
171
137
  tau_key = 'tau_mean_int' if tau_weighting == 'int' else 'tau_mean_amp'
172
138
  if tau_key not in canvas:
173
139
  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
140
  intensity = canvas['intensity']
179
- pos = intensity[intensity > 0]
141
+ pos = intensity[intensity > 0]
180
142
  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
143
  i_min = float(intensity_display_min) if intensity_display_min is not None else 0.0
184
144
  i_max = float(intensity_display_max) if intensity_display_max is not None else float(np.percentile(pos, 99.0))
185
145
  i_max = max(i_max, i_min + 1e-6)
@@ -190,65 +150,51 @@ def save_assembled_maps(
190
150
  else:
191
151
  intensity_scaled = np.zeros_like(intensity, dtype=np.uint16)
192
152
  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.
153
+ tifffile.imwrite(str(output_dir / f'{roi_name}_intensity.tif'), intensity_scaled, **tiff_resolution(intensity_scaled, field_area_um2))
154
+ print(f' intensity display range: {i_min:.1f} - {i_max:.1f} counts')
201
155
  tau_map = canvas[tau_key]
202
- finite = np.isfinite(tau_map)
203
-
156
+ finite = np.isfinite(tau_map)
204
157
  t_min = float(tau_display_min) if tau_display_min is not None \
205
158
  else float(np.nanpercentile(tau_map[finite], 0.5) if finite.any() else 0.0)
206
159
  t_max = float(tau_display_max) if tau_display_max is not None \
207
160
  else float(np.nanpercentile(tau_map[finite], 99.5) if finite.any() else 1.0)
208
161
  t_max = max(t_max, t_min + 1e-6)
209
-
210
162
  tau_u16 = np.where(
211
163
  finite,
212
164
  np.clip((tau_map - t_min) / (t_max - t_min) * 65535, 0, 65535),
213
165
  0
214
166
  ).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"
167
+ tifffile.imwrite(str(output_dir / f'{roi_name}_{tau_key}.tif'), tau_u16, **tiff_resolution(tau_u16, field_area_um2))
168
+ print(f' {tau_key} display range: {t_min:.3f} - {t_max:.3f} ns '
169
+ f'→ 0 - 65535')
170
+ summary_path = output_dir / f'{roi_name}_global_summary.txt'
221
171
  with open(summary_path, 'w') as f:
222
172
  f.write('=' * 60 + '\n')
223
173
  f.write('PER-TILE FIT - GLOBAL TAU SUMMARY\n')
224
174
  f.write('=' * 60 + '\n\n')
225
- f.write(f"ROI: {roi_name}\n")
175
+ f.write(f'ROI: {roi_name}\n')
226
176
  n_pixels_fitted = global_summary.get('n_pixels_fitted', 'N/A')
227
177
  if isinstance(n_pixels_fitted, int):
228
- f.write(f"Pixels fitted: {n_pixels_fitted:,}\n\n")
178
+ f.write(f'Pixels fitted: {n_pixels_fitted:,}\n\n')
229
179
  else:
230
- f.write(f"Pixels fitted: {n_pixels_fitted}\n\n")
180
+ f.write(f'Pixels fitted: {n_pixels_fitted}\n\n')
231
181
  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")
182
+ f.write(f" tau_mean = {global_summary.get('tau_mean_amp_global_ns', float('nan')):.4f} ns\n")
183
+ f.write(f" tau_std = {global_summary.get('tau_std_amp_global_ns', float('nan')):.4f} ns\n")
234
184
  f.write(f" tau_median= {global_summary.get('tau_median_amp_global_ns', float('nan')):.4f} ns\n\n")
235
185
  f.write('Per-component (amplitude-weighted across ROI):\n')
236
186
  for k in range(1, n_exp + 1):
237
187
  tau_k = global_summary.get(f'tau{k}_mean_ns', None)
238
- a_k = global_summary.get(f'a{k}_mean_frac', None)
188
+ a_k = global_summary.get(f'a{k}_mean_frac', None)
239
189
  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")
190
+ f.write(f' tau{k} = {tau_k:.4f} ns '
191
+ f'a{k} = {a_k:.3f} (mean amplitude fraction)\n')
242
192
  f.write('\n' + '=' * 60 + '\n')
243
-
244
- # Component RGB TIFF
245
193
  try:
246
194
  from ..utils.lifetime_image import make_component_rgb_tiff
247
- make_component_rgb_tiff(canvas, output_dir, roi_name, n_exp, verbose=True)
195
+ make_component_rgb_tiff(canvas, output_dir, roi_name, n_exp, verbose=True, field_area_um2=field_area_um2)
248
196
  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
197
+ print(f' component RGB skipped: {_e}')
198
+ print(f' Assembled maps saved to {output_dir}')
199
+ print(f' Global summary: {summary_path.name}')
200
+ return summary_path
@@ -9,6 +9,7 @@ from flimkit.formats import FLIMFile
9
9
  from ..FLIM.fitters import fit_summed, fit_per_pixel
10
10
  from ..FLIM.fit_tools import find_irf_peak_bin
11
11
  from ..utils.lifetime_image import make_lifetime_image, make_component_rgb_tiff
12
+ from ..utils.export_png import flim_field_area_um2, tiff_resolution
12
13
  from ..utils.plotting import plot_summed
13
14
  from ..configs import (
14
15
  MIN_PHOTONS_PERPIX, lm_restarts, de_population, de_maxiter, n_workers,
@@ -200,6 +201,7 @@ def fit_timelapse(ptu_dir, output_dir, args,
200
201
  )
201
202
  redox = compute_redox_metrics(pixel_maps, args.nexp,
202
203
  compute_bound_fraction=compute_bound_fraction)
204
+ area_um2 = flim_field_area_um2(ptu)
203
205
  pos_dir = frame_dir / f's{s}'
204
206
  pos_dir.mkdir(exist_ok=True)
205
207
  intensity = pixel_maps.get('intensity', pixel_stack.sum(axis=2))
@@ -218,6 +220,7 @@ def fit_timelapse(ptu_dir, output_dir, args,
218
220
  canvas=pixel_maps, output_dir=pos_dir, roi_name=roi_name,
219
221
  tau_min_ns=tau_disp_min, tau_max_ns=tau_disp_max,
220
222
  intensity_percentile_hi=95, tau_key='tau_mean_int', verbose=False,
223
+ field_area_um2=area_um2,
221
224
  )
222
225
  except Exception as exc:
223
226
  print(f' Warning: lifetime image export failed for {roi_name}: {exc}')
@@ -258,6 +261,7 @@ def fit_timelapse(ptu_dir, output_dir, args,
258
261
  make_component_rgb_tiff(
259
262
  canvas=pixel_maps, output_dir=pos_dir, roi_name=roi_name,
260
263
  n_exp=args.nexp, intensity_percentile_hi=95, verbose=False,
264
+ field_area_um2=area_um2,
261
265
  )
262
266
  except Exception as exc:
263
267
  print(f' Warning: component RGB TIFF export failed for {roi_name}: {exc}')
@@ -272,14 +276,16 @@ def fit_timelapse(ptu_dir, output_dir, args,
272
276
  intensity_u16 = np.clip(
273
277
  intensity.astype(np.float64) / i_max * 65535, 0, 65535
274
278
  ).astype(np.uint16)
275
- _tifffile.imwrite(str(pos_dir / f'{roi_name}_intensity.tif'), intensity_u16)
279
+ _tifffile.imwrite(str(pos_dir / f'{roi_name}_intensity.tif'), intensity_u16,
280
+ **tiff_resolution(intensity_u16, area_um2))
276
281
  except Exception as exc:
277
282
  print(f' Warning: intensity TIFF export failed for {roi_name}: {exc}')
278
283
  if getattr(args, 'save_ind', False):
279
284
  try:
280
285
  from ..utils.enhanced_outputs import save_individual_tau_maps
281
286
  save_individual_tau_maps(
282
- pixel_maps, pos_dir, roi_name=roi_name, n_exp=args.nexp)
287
+ pixel_maps, pos_dir, roi_name=roi_name, n_exp=args.nexp,
288
+ field_area_um2=area_um2)
283
289
  except Exception as exc:
284
290
  print(f' Warning: individual component map export failed for {roi_name}: {exc}')
285
291
  stats = {'t': t, 's': s, 'path': str(ptu_path)}
@@ -528,6 +534,7 @@ def fit_zstack(ptu_dir, output_dir, args,
528
534
  )
529
535
  redox = compute_redox_metrics(pixel_maps, args.nexp,
530
536
  compute_bound_fraction=compute_bound_fraction)
537
+ area_um2 = flim_field_area_um2(ptu)
531
538
  intensity = pixel_maps.get('intensity', pixel_stack.sum(axis=2))
532
539
  np.save(str(slice_dir / 'intensity.npy'), intensity.astype(np.float32))
533
540
  for map_name in ('alpha_1', 'alpha_2', 'alpha_3', 'tau_mean_amp',
@@ -544,6 +551,7 @@ def fit_zstack(ptu_dir, output_dir, args,
544
551
  canvas=pixel_maps, output_dir=slice_dir, roi_name=roi_name,
545
552
  tau_min_ns=tau_disp_min, tau_max_ns=tau_disp_max,
546
553
  intensity_percentile_hi=95, tau_key='tau_mean_int', verbose=False,
554
+ field_area_um2=area_um2,
547
555
  )
548
556
  if png_path is not None and Path(png_path).exists():
549
557
  shutil.copyfile(str(png_path), str(preview_dir / f'z{z:04d}.png'))
@@ -586,6 +594,7 @@ def fit_zstack(ptu_dir, output_dir, args,
586
594
  make_component_rgb_tiff(
587
595
  canvas=pixel_maps, output_dir=slice_dir, roi_name=roi_name,
588
596
  n_exp=args.nexp, intensity_percentile_hi=95, verbose=False,
597
+ field_area_um2=area_um2,
589
598
  )
590
599
  except Exception as exc:
591
600
  print(f' Warning: component RGB TIFF export failed for {roi_name}: {exc}')
@@ -600,14 +609,16 @@ def fit_zstack(ptu_dir, output_dir, args,
600
609
  intensity_u16 = np.clip(
601
610
  intensity.astype(np.float64) / i_max * 65535, 0, 65535
602
611
  ).astype(np.uint16)
603
- _tifffile.imwrite(str(slice_dir / f'{roi_name}_intensity.tif'), intensity_u16)
612
+ _tifffile.imwrite(str(slice_dir / f'{roi_name}_intensity.tif'), intensity_u16,
613
+ **tiff_resolution(intensity_u16, area_um2))
604
614
  except Exception as exc:
605
615
  print(f' Warning: intensity TIFF export failed for {roi_name}: {exc}')
606
616
  if getattr(args, 'save_ind', False):
607
617
  try:
608
618
  from ..utils.enhanced_outputs import save_individual_tau_maps
609
619
  save_individual_tau_maps(
610
- pixel_maps, slice_dir, roi_name=roi_name, n_exp=args.nexp)
620
+ pixel_maps, slice_dir, roi_name=roi_name, n_exp=args.nexp,
621
+ field_area_um2=area_um2)
611
622
  except Exception as exc:
612
623
  print(f' Warning: individual component map export failed for {roi_name}: {exc}')
613
624
  stats = {'z': z, 'path': str(ptu_path)}