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,296 @@
1
+ import numpy as np
2
+ from pathlib import Path
3
+
4
+ _LASER_RATES_HZ = (20e6, 50e6, 80e6)
5
+ _DECAY_CONTENT = (0x10, 0x60)
6
+ _INT_CONTENT = 0xa0
7
+
8
+ def _load_sdtfile():
9
+ try:
10
+ import sdtfile
11
+ except ImportError:
12
+ raise ImportError('sdtfile is required to read Becker & Hickl .sdt files '
13
+ '(pip install sdtfile)')
14
+ return sdtfile
15
+
16
+ def _next_pow2(n):
17
+ return 1 << (int(n) - 1).bit_length() if n > 1 else 1
18
+
19
+ def _reshape_cube(arr, image_x, image_y, adc_re):
20
+ flat = np.ascontiguousarray(arr).reshape(-1)
21
+ per_pixel = adc_re if adc_re > 0 else 1
22
+ n_pixels = flat.size // per_pixel
23
+ if n_pixels == image_x * image_y:
24
+ out = flat[:image_y * image_x * per_pixel].reshape(image_y, image_x, per_pixel)
25
+ else:
26
+ xpad, ypad = _next_pow2(image_x), _next_pow2(image_y)
27
+ out = flat[:ypad * xpad * per_pixel].reshape(ypad, xpad, per_pixel)
28
+ out = out[:image_y, :image_x, :]
29
+ return np.ascontiguousarray(out)
30
+
31
+ def _reshape_intensity(arr, image_x, image_y):
32
+ flat = np.ascontiguousarray(arr).reshape(-1)
33
+ if flat.size == image_x * image_y:
34
+ out = flat.reshape(image_y, image_x)
35
+ else:
36
+ xpad, ypad = _next_pow2(image_x), _next_pow2(image_y)
37
+ out = flat[:ypad * xpad].reshape(ypad, xpad)[:image_y, :image_x]
38
+ return np.ascontiguousarray(out)
39
+
40
+ def _decode_str(value):
41
+ if isinstance(value, bytes):
42
+ return value.split(b'\x00', 1)[0].decode('ascii', 'replace').strip()
43
+ return str(value).strip().strip('\x04').strip()
44
+
45
+ class BHFile:
46
+ def __init__(self, path, verbose=True, channel=None, sync_rate=None):
47
+ self.path = str(path)
48
+ self.verbose = verbose
49
+ self._sync_override = float(sync_rate) if sync_rate else None
50
+ sdtfile = _load_sdtfile()
51
+ self._sdt = sdtfile.SdtFile(self.path)
52
+ headers = self._sdt.block_headers
53
+ self._decay_idx = [i for i, b in enumerate(headers)
54
+ if (int(b['block_type']) & 0x00f0) in _DECAY_CONTENT]
55
+ self._int_idx = [i for i, b in enumerate(headers)
56
+ if (int(b['block_type']) & 0x00f0) == _INT_CONTENT]
57
+ if not self._decay_idx:
58
+ raise ValueError(f'No FLIM image/decay blocks found in {self.path}')
59
+ self._cube_cache = {}
60
+ self.n_x = None
61
+ self.n_y = None
62
+ self._total_photons = None
63
+ self._parse_meta(channel)
64
+
65
+ def _measure_info(self, data_idx):
66
+ mi = self._sdt.measure_info
67
+ no = int(self._sdt.block_headers[data_idx]['meas_desc_block_no'])
68
+ return mi[no] if no < len(mi) else mi[0]
69
+
70
+ def close(self):
71
+ try:
72
+ self._sdt.close()
73
+ except Exception:
74
+ pass
75
+
76
+ def __enter__(self):
77
+ return self
78
+
79
+ def __exit__(self, exc_type, exc, tb):
80
+ self.close()
81
+
82
+ def _parse_meta(self, channel):
83
+ self.n_channels = len(self._decay_idx)
84
+ if channel is not None:
85
+ self.photon_channel = int(channel)
86
+ else:
87
+ self.photon_channel = 1 if self.n_channels == 1 else None
88
+ mi = self._measure_info(self._decay_idx[0])
89
+ self.module_type = _decode_str(mi['mod_type'])
90
+ adc_re = int(mi['adc_re'])
91
+ self.n_bins = adc_re if adc_re != 0 else 65536
92
+ tac_r = float(mi['tac_r'])
93
+ tac_g = int(mi['tac_g'])
94
+ if tac_g and self.n_bins:
95
+ self.tcspc_res = tac_r / (tac_g * self.n_bins)
96
+ else:
97
+ self.tcspc_res = 0.0
98
+ self.time_ns = (np.arange(self.n_bins) + 0.5) * self.tcspc_res * 1e9
99
+ stop = mi['StopInfo']
100
+ self.min_sync_rate = float(stop['min_sync_rate'])
101
+ self.max_sync_rate = float(stop['max_sync_rate'])
102
+ if self._sync_override and self._sync_override > 0:
103
+ self.sync_rate = self._sync_override
104
+ self.sync_source = 'user'
105
+ else:
106
+ sync = self.max_sync_rate if self.max_sync_rate > 0 else self.min_sync_rate
107
+ self.sync_rate = float(sync) if sync and sync > 0 else 0.0
108
+ self.sync_source = 'measured'
109
+ self.period_ns = (1e9 / self.sync_rate) if self.sync_rate > 0 else 0.0
110
+ self.n_records = 0
111
+ self.col_time_s = float(mi['col_t']) or None
112
+ self.acq_time_s = float(stop['stop_time']) or self.col_time_s
113
+ if self.sync_rate > 0 and self.acq_time_s:
114
+ self.n_sync = int(self.sync_rate * self.acq_time_s)
115
+ else:
116
+ self.n_sync = None
117
+ info = self._sdt.info
118
+ self.tags = {
119
+ 'BH_ID': _decode_str(getattr(info, 'id', '')),
120
+ 'BH_Title': _decode_str(getattr(info, 'title', '')),
121
+ 'BH_Date': _decode_str(getattr(info, 'date', '')),
122
+ 'BH_Time': _decode_str(getattr(info, 'time', '')),
123
+ 'BH_ModuleType': self.module_type,
124
+ 'BH_MeasMode': int(mi['meas_mode']),
125
+ 'BH_ADCResolution': self.n_bins,
126
+ 'BH_TACRange_s': tac_r,
127
+ 'BH_TACGain': tac_g,
128
+ 'BH_ImageX': int(mi['image_x']),
129
+ 'BH_ImageY': int(mi['image_y']),
130
+ 'BH_CollectionTime_s': float(mi['col_t']),
131
+ 'BH_MinSyncRate_Hz': self.min_sync_rate,
132
+ 'BH_MaxSyncRate_Hz': self.max_sync_rate,
133
+ 'BH_SyncRate_Hz': self.sync_rate,
134
+ 'BH_SyncRateSource': self.sync_source,
135
+ 'BH_Channels': self.n_channels,
136
+ }
137
+ if self.verbose:
138
+ print(f" B&H SPC : {Path(self.path).name}")
139
+ print(f" Module : {self.module_type} ({self.n_channels} channel(s))")
140
+ print(f" TCSPC : {self.n_bins} bins x {self.tcspc_res*1e12:.2f} ps")
141
+ sync_mhz = self.sync_rate / 1e6
142
+ print(f" Sync : {sync_mhz:.2f} MHz (period {self.period_ns:.3f} ns, {self.sync_source})")
143
+ if (self.sync_source == 'measured' and self.min_sync_rate > 0
144
+ and self.max_sync_rate > 0
145
+ and abs(self.max_sync_rate - self.min_sync_rate) > 0.01 * self.max_sync_rate):
146
+ print(f" WARNING: min/max sync differ "
147
+ f"({self.min_sync_rate/1e6:.3f} vs {self.max_sync_rate/1e6:.3f} MHz), using max")
148
+ if self.sync_rate > 0:
149
+ nearest = min(_LASER_RATES_HZ, key=lambda r: abs(r - self.sync_rate))
150
+ if abs(nearest - self.sync_rate) > 0.02 * nearest:
151
+ print(f" NOTE : {sync_mhz:.2f} MHz is non-standard "
152
+ f"(period {self.period_ns:.3f} ns from measured sync); "
153
+ f"pass sync_rate=<Hz> to override if the laser differs")
154
+ print(' ')
155
+
156
+ @property
157
+ def photons_per_pulse(self):
158
+ if self._total_photons is None or not self.n_sync:
159
+ return None
160
+ return self._total_photons / self.n_sync
161
+
162
+ def _norm_channel(self, channel):
163
+ if channel is None or int(channel) < 1:
164
+ return self._ensure_photon_channel()
165
+ return int(channel)
166
+
167
+ def _ensure_photon_channel(self):
168
+ if self.photon_channel is None:
169
+ self.photon_channel = self._select_photon_channel() if self.n_channels > 1 else 1
170
+ return self.photon_channel
171
+
172
+ def _select_photon_channel(self):
173
+ best_idx, best_sum, total_all = 1, -1, 0
174
+ for idx in range(1, self.n_channels + 1):
175
+ cube = self._decode_cube_for(idx)
176
+ total = int(cube.sum())
177
+ total_all += total
178
+ if total > best_sum:
179
+ self._cube_cache.pop(best_idx, None)
180
+ best_idx, best_sum = idx, total
181
+ self._cube_cache[idx] = cube
182
+ if self.verbose:
183
+ share = (100.0 * best_sum / total_all) if total_all > 0 else 0.0
184
+ print(f" Channel : auto-selected {best_idx}/{self.n_channels} "
185
+ f"({best_sum:,} photons, {share:.1f}% of total)")
186
+ return best_idx
187
+
188
+ def _decode_cube_for(self, idx):
189
+ data_idx = self._decay_idx[idx - 1]
190
+ mi = self._measure_info(data_idx)
191
+ return _reshape_cube(self._sdt.data[data_idx], int(mi['image_x']),
192
+ int(mi['image_y']), self.n_bins)
193
+
194
+ def _decode_cube(self, channel):
195
+ idx = self._norm_channel(channel)
196
+ if idx in self._cube_cache:
197
+ return self._cube_cache[idx]
198
+ if idx < 1 or idx > self.n_channels:
199
+ raise ValueError(f'Channel {channel} out of range 1..{self.n_channels}')
200
+ cube = self._decode_cube_for(idx)
201
+ self._cube_cache[idx] = cube
202
+ return cube
203
+
204
+ def pixel_stack(self, channel=None, binning=1):
205
+ cube = self._decode_cube(channel)
206
+ if binning > 1:
207
+ cube = _bin_cube(cube, binning)
208
+ self.n_y, self.n_x = cube.shape[0], cube.shape[1]
209
+ self._total_photons = int(cube.sum())
210
+ self.n_records = self._total_photons
211
+ if self.verbose:
212
+ print(f' Built cube {cube.shape} ({self._total_photons:,} photons)')
213
+ return cube.astype(np.uint32)
214
+
215
+ def raw_pixel_stack(self, channel=None, binning=1):
216
+ return self.pixel_stack(channel=channel, binning=binning)
217
+
218
+ def summed_decay(self, channel=None):
219
+ cube = self._decode_cube(channel)
220
+ decay = cube.sum(axis=(0, 1)).astype(float)
221
+ self._total_photons = int(decay.sum())
222
+ return decay
223
+
224
+ def intensity_image(self, channel=None, binning=1):
225
+ idx = self._norm_channel(channel)
226
+ decay_idx = self._decay_idx[idx - 1]
227
+ md = int(self._sdt.block_headers[decay_idx]['meas_desc_block_no'])
228
+ match = [i for i in self._int_idx
229
+ if int(self._sdt.block_headers[i]['meas_desc_block_no']) == md]
230
+ if match:
231
+ mi = self._measure_info(match[0])
232
+ img = _reshape_intensity(self._sdt.data[match[0]],
233
+ int(mi['image_x']), int(mi['image_y'])).astype(np.uint64)
234
+ else:
235
+ return self.pixel_stack(channel=idx, binning=binning).sum(axis=2)
236
+ if binning > 1:
237
+ ny, nx = img.shape
238
+ ny2, nx2 = (ny // binning) * binning, (nx // binning) * binning
239
+ img = img[:ny2, :nx2].reshape(ny2 // binning, binning,
240
+ nx2 // binning, binning).sum(axis=(1, 3))
241
+ return img
242
+
243
+ def _bin_cube(cube, binning):
244
+ if binning <= 1:
245
+ return cube
246
+ ny, nx, nh = cube.shape
247
+ ny2, nx2 = (ny // binning) * binning, (nx // binning) * binning
248
+ cube = cube[:ny2, :nx2, :]
249
+ return cube.reshape(ny2 // binning, binning, nx2 // binning, binning, nh).sum(axis=(1, 3))
250
+
251
+ def _metadata(bh, data):
252
+ return {
253
+ 'frequency': bh.sync_rate,
254
+ 'tcspc_resolution': bh.tcspc_res,
255
+ 'shape': data.shape,
256
+ 'dims': ('Y', 'X', 'H'),
257
+ 'tags': bh.tags,
258
+ 'x_pixel_size': 0,
259
+ 'y_pixel_size': 0,
260
+ 'n_bins': bh.n_bins,
261
+ 'time_ns': bh.time_ns,
262
+ 'photon_channel': bh.photon_channel,
263
+ }
264
+
265
+ def read_bh(path, binning=1, channel=None, verbose=False, sync_rate=None):
266
+ bh = BHFile(path, verbose=verbose, channel=channel, sync_rate=sync_rate)
267
+ data = bh.pixel_stack(channel=channel, binning=binning)
268
+ metadata = _metadata(bh, data)
269
+ bh.close()
270
+ return data, metadata
271
+
272
+ def get_flim_data(path, binning=1, channel=None, sync_rate=None):
273
+ return read_bh(path, binning=binning, channel=channel, verbose=False, sync_rate=sync_rate)
274
+
275
+ def get_intensity_image(path, binning=1, channel=None, sync_rate=None):
276
+ bh = BHFile(path, verbose=False, channel=channel, sync_rate=sync_rate)
277
+ img = bh.intensity_image(channel=channel, binning=binning)
278
+ cube_shape = (bh.n_y, bh.n_x, bh.n_bins)
279
+ metadata = _metadata(bh, np.empty(0))
280
+ metadata['shape'] = cube_shape
281
+ bh.close()
282
+ return img, metadata
283
+
284
+ def normalise_flim(flim):
285
+ if flim is None:
286
+ return None
287
+ if flim.ndim == 5:
288
+ return flim[0, :, :, 0, :]
289
+ if flim.ndim == 4:
290
+ return flim[0] if flim.shape[0] == 1 else flim[:, :, 0, :]
291
+ if flim.ndim == 3:
292
+ return flim
293
+ return None
294
+
295
+ def create_time_axis(n_bins, tcspc_resolution):
296
+ return np.arange(n_bins) * tcspc_resolution * 1e9
@@ -0,0 +1,86 @@
1
+ import numpy as np
2
+
3
+ def _records():
4
+ from sdtfile.sdtfile import FILE_HEADER, BLOCK_HEADER, MEASURE_INFO
5
+ return (np.dtype(FILE_HEADER), np.dtype(BLOCK_HEADER), np.dtype(MEASURE_INFO))
6
+
7
+ def _measure_info_bytes(mi_dtype, n_bins, tcspc_res_ns, period_ns, image_x, image_y,
8
+ col_time_s, module='SPC-150'):
9
+ mi = np.zeros(1, mi_dtype)
10
+ tac_g = 1
11
+ mi['adc_re'] = int(n_bins)
12
+ mi['tac_g'] = tac_g
13
+ mi['tac_r'] = float(tcspc_res_ns * 1e-9 * tac_g * n_bins)
14
+ mi['image_x'] = int(image_x)
15
+ mi['image_y'] = int(image_y)
16
+ mi['scan_x'] = int(image_x)
17
+ mi['scan_y'] = int(image_y)
18
+ mi['meas_mode'] = 13
19
+ mi['col_t'] = float(col_time_s)
20
+ mi['mod_type'] = module.encode('ascii')[:16]
21
+ sync = (1e9 / period_ns) if period_ns > 0 else 0.0
22
+ si = mi['StopInfo']
23
+ si['stop_time'] = float(col_time_s)
24
+ si['min_sync_rate'] = float(sync)
25
+ si['max_sync_rate'] = float(sync)
26
+ return mi.tobytes()
27
+
28
+ def _file_info_bytes(title='FLIMKit synthetic', date='2026-01-01', time='00:00:00'):
29
+ lines = [
30
+ '*IDENTIFICATION',
31
+ 'ID : SPC Setup & Data File',
32
+ f'Title : {title}',
33
+ 'Version : 1 850 M',
34
+ f'Date : {date}',
35
+ f'Time : {time}',
36
+ '*END',
37
+ '',
38
+ ]
39
+ return ('\r\n'.join(lines)).encode('windows-1250')
40
+
41
+ def write_sdt(path, cube, period_ns, tcspc_res_ns, col_time_s=10.0,
42
+ title='FLIMKit synthetic', module='SPC-150'):
43
+ cube = np.ascontiguousarray(cube)
44
+ if cube.ndim != 3:
45
+ raise ValueError('cube must be (Y, X, H)')
46
+ ny, nx, nbins = cube.shape
47
+ data = np.clip(cube, 0, 65535).astype('<u2')
48
+ data_bytes = data.tobytes()
49
+ fh_dtype, bh_dtype, mi_dtype = _records()
50
+ info = _file_info_bytes(title=title)
51
+ mi_bytes = _measure_info_bytes(mi_dtype, nbins, tcspc_res_ns, period_ns,
52
+ nx, ny, col_time_s, module=module)
53
+ header_size = fh_dtype.itemsize
54
+ bh_size = bh_dtype.itemsize
55
+ info_offs = header_size
56
+ meas_offs = info_offs + len(info)
57
+ data_block_offs = meas_offs + len(mi_bytes)
58
+ data_offs = data_block_offs + bh_size
59
+ next_block_offs = data_offs + len(data_bytes)
60
+ bh = np.zeros(1, bh_dtype)
61
+ bh['data_offs'] = data_offs
62
+ bh['next_block_offs'] = next_block_offs
63
+ bh['block_type'] = 0x0061
64
+ bh['meas_desc_block_no'] = 0
65
+ bh['lblock_no'] = 1
66
+ bh['block_length'] = len(data_bytes)
67
+ fh = np.zeros(1, fh_dtype)
68
+ fh['revision'] = 15
69
+ fh['info_offs'] = info_offs
70
+ fh['info_length'] = len(info)
71
+ fh['setup_offs'] = 0
72
+ fh['setup_length'] = 0
73
+ fh['data_block_offs'] = data_block_offs
74
+ fh['no_of_data_blocks'] = 1
75
+ fh['data_block_length'] = len(data_bytes)
76
+ fh['meas_desc_block_offs'] = meas_offs
77
+ fh['no_of_meas_desc_blocks'] = 1
78
+ fh['meas_desc_block_length'] = len(mi_bytes)
79
+ fh['header_valid'] = 0x5555
80
+ with open(path, 'wb') as f:
81
+ f.write(fh.tobytes())
82
+ f.write(info)
83
+ f.write(mi_bytes)
84
+ f.write(bh.tobytes())
85
+ f.write(data_bytes)
86
+ return str(path)
File without changes
@@ -0,0 +1,86 @@
1
+ import numpy as np
2
+ from lfdfiles import VistaIfli
3
+
4
+ def _to_hz(f):
5
+ return float(f) if f > 1e5 else float(f) * 1e6
6
+
7
+ class ISSFdFlim:
8
+ modality = 'frequency'
9
+
10
+ def __init__(self, path, verbose=True, channel=None, **kwargs):
11
+ self.path = str(path)
12
+ self.verbose = verbose
13
+ self.channel = channel
14
+ with VistaIfli(self.path) as f:
15
+ self.header = dict(f.header)
16
+ self.axes = f.axes
17
+ data = f.asarray()
18
+ sizer, sizee, sizet, sizec, sizez, sizey, sizex, sizef, _ = data.shape
19
+ self.n_x = int(sizex)
20
+ self.n_y = int(sizey)
21
+ self.n_z = max(int(sizez), 1)
22
+ self.n_channels = max(int(sizec), 1)
23
+ self.n_time_series = max(int(sizet), 1)
24
+ self.n_freq = max(int(sizef), 1)
25
+ self.n_positions = max(int(sizer), 1)
26
+ self.n_spectral = max(int(sizee), 1)
27
+ self._data = data
28
+ self._mod_freqs = np.asarray(self.header.get('ModFrequency', ()), dtype=np.float32)
29
+ self._ref_lifetime_ns = np.asarray(self.header.get('RefLifetime', ()), dtype=np.float32)
30
+ self._ref_phasor = np.asarray(self.header.get('RefDCPhasor'))
31
+ if self.verbose:
32
+ print(f' ISS FD-FLIM : {self.path}')
33
+ print(f' Size : {self.n_x} x {self.n_y} px, {self.n_channels} ch, '
34
+ f'{self.n_freq} freq, {self.n_time_series} time series')
35
+ print(' Frequency-domain data: loads into phasor analysis, no lifetime fitting')
36
+
37
+ def _channel_index(self, channel):
38
+ if channel is None:
39
+ return 0
40
+ c = int(channel)
41
+ idx = c - 1 if c >= 1 else c
42
+ return idx if 0 <= idx < self.n_channels else 0
43
+
44
+ def phasor(self, channel=None, harmonic=0, calibrate=True, time_series=0):
45
+ ci = self._channel_index(self.channel if channel is None else channel)
46
+ fi = int(harmonic) if 0 <= int(harmonic) < self.n_freq else 0
47
+ ts = int(time_series) if 0 <= int(time_series) < self.n_time_series else 0
48
+ block = self._data[0, 0, ts, ci].sum(axis=0)
49
+ dc = block[:, :, fi, 0]
50
+ gx = block[:, :, fi, 1]
51
+ gy = block[:, :, fi, 2]
52
+ freq_hz = _to_hz(self._mod_freqs[fi]) if self._mod_freqs.size else 0.0
53
+ if calibrate:
54
+ gx, gy = self._calibrate(gx, gy, ci, fi, freq_hz)
55
+ return dc.astype(float), gx.astype(float), gy.astype(float), freq_hz / 1e6
56
+
57
+ def _calibrate(self, gx, gy, ci, fi, freq_hz):
58
+ tau_ref = float(self._ref_lifetime_ns[ci]) * 1e-9 if ci < self._ref_lifetime_ns.size else 0.0
59
+ rx = ry = 0.0
60
+ if self._ref_phasor is not None and self._ref_phasor.ndim == 3 \
61
+ and ci < self._ref_phasor.shape[0] and fi < self._ref_phasor.shape[1]:
62
+ rx = float(self._ref_phasor[ci, fi, 1])
63
+ ry = float(self._ref_phasor[ci, fi, 2])
64
+ if tau_ref <= 0 or (rx == 0 and ry == 0) or freq_hz <= 0:
65
+ return gx, gy
66
+ w = 2.0 * np.pi * freq_hz
67
+ denom = 1.0 + (w * tau_ref) ** 2
68
+ gt, st = 1.0 / denom, (w * tau_ref) / denom
69
+ scale = np.hypot(gt, st) / np.hypot(rx, ry)
70
+ dphi = np.arctan2(st, gt) - np.arctan2(ry, rx)
71
+ cos, sin = np.cos(dphi), np.sin(dphi)
72
+ return scale * (gx * cos - gy * sin), scale * (gx * sin + gy * cos)
73
+
74
+ def pixel_stack(self, *args, **kwargs):
75
+ raise ValueError('ISS .ifli is frequency-domain (FD-FLIM): there is no '
76
+ 'time-domain decay to fit. Load it into phasor analysis instead.')
77
+
78
+ def summed_decay(self, *args, **kwargs):
79
+ return self.pixel_stack()
80
+
81
+ def raw_pixel_stack(self, *args, **kwargs):
82
+ return self.pixel_stack()
83
+
84
+ def phasor_from_ifli(path, channel=None, harmonic=0, calibrate=True, verbose=False):
85
+ return ISSFdFlim(path, verbose=verbose).phasor(channel=channel, harmonic=harmonic,
86
+ calibrate=calibrate)
@@ -0,0 +1,114 @@
1
+ import struct
2
+ import numpy as np
3
+ from pathlib import Path
4
+
5
+ _HEADER_BYTES = 256
6
+ _SIGNATURE = b'VISTAIMAGE'
7
+
8
+ def _read_header(buf):
9
+ h = {}
10
+ h['signature'] = bytes(buf[0:10])
11
+ h['file_version'] = buf[10]
12
+ h['image_type'] = buf[11]
13
+ h['channel_bits'] = buf[12]
14
+ h['compression'] = buf[13]
15
+ h['res_x'] = struct.unpack_from('<H', buf, 14)[0]
16
+ h['res_y'] = struct.unpack_from('<H', buf, 16)[0]
17
+ h['res_z'] = struct.unpack_from('<H', buf, 18)[0]
18
+ region = struct.unpack_from('<6f', buf, 20)
19
+ h['region'] = {'x': region[0:2], 'y': region[2:4], 'z': region[4:6]}
20
+ h['pixel_acq_time'] = struct.unpack_from('<f', buf, 44)[0]
21
+ h['n_channels'] = struct.unpack_from('<H', buf, 93)[0]
22
+ h['n_time_series'] = struct.unpack_from('<H', buf, 95)[0]
23
+ h['pixel_interval'] = struct.unpack_from('<f', buf, 97)[0]
24
+ h['line_interval'] = struct.unpack_from('<f', buf, 101)[0]
25
+ h['frame_interval'] = struct.unpack_from('<f', buf, 105)[0]
26
+ h['n_frames_integrated'] = struct.unpack_from('<H', buf, 109)[0]
27
+ return h
28
+
29
+ class ISSImage:
30
+ def __init__(self, path, verbose=True, byteorder='<'):
31
+ self.path = str(path)
32
+ self.verbose = verbose
33
+ with open(self.path, 'rb') as fh:
34
+ buf = fh.read(_HEADER_BYTES)
35
+ data = np.frombuffer(fh.read(), dtype=np.dtype(byteorder + 'f4'))
36
+ if len(buf) < _HEADER_BYTES:
37
+ raise ValueError(f'ISS .ifi header truncated: {self.path}')
38
+ self.header = _read_header(buf)
39
+ if self.header['signature'][:10] != _SIGNATURE and self.verbose:
40
+ print(f" WARNING: ISS .ifi signature {self.header['signature']!r} != {_SIGNATURE!r}")
41
+ self._parse(data)
42
+
43
+ def _parse(self, data):
44
+ h = self.header
45
+ self.n_x = int(h['res_x'])
46
+ self.n_y = int(h['res_y'])
47
+ self.n_z = int(h['res_z'])
48
+ nc = int(h['n_channels'])
49
+ if nc <= 0:
50
+ nc = bin(int(h['channel_bits'])).count('1') or 1
51
+ per_frame = self.n_x * self.n_y
52
+ self.n_frames = (data.size // (nc * per_frame)) if nc * per_frame > 0 else 0
53
+ self.n_channels = nc
54
+ keep = nc * self.n_frames * per_frame
55
+ if keep > 0:
56
+ self.images = data[:keep].reshape(nc, self.n_frames, self.n_y, self.n_x)
57
+ else:
58
+ self.images = np.zeros((nc, 0, self.n_y, self.n_x), dtype=np.float32)
59
+ self.tags = {
60
+ 'ISS_FileVersion': int(h['file_version']),
61
+ 'ISS_ChannelBits': int(h['channel_bits']),
62
+ 'ISS_ResolutionXYZ': (self.n_x, self.n_y, self.n_z),
63
+ 'ISS_NumChannels': self.n_channels,
64
+ 'ISS_NumFrames': self.n_frames,
65
+ 'ISS_NumTimeSeries': int(h['n_time_series']),
66
+ 'ISS_PixelAcqTime': float(h['pixel_acq_time']),
67
+ 'ISS_Region': h['region'],
68
+ }
69
+ if self.verbose:
70
+ print(f' ISS image : {Path(self.path).stem}')
71
+ print(f' Size : {self.n_x} x {self.n_y} px, {self.n_channels} ch, {self.n_frames} frame(s)')
72
+ print(' ')
73
+
74
+ def _channel_index(self, channel):
75
+ c = int(channel)
76
+ idx = c - 1 if c >= 1 else c
77
+ if idx < 0 or idx >= self.n_channels:
78
+ return 0
79
+ return idx
80
+
81
+ def image(self, channel=None, frame=None):
82
+ if self.n_channels == 0 or self.n_frames == 0:
83
+ return np.zeros((self.n_y, self.n_x), dtype=np.float32)
84
+ ci = 0 if channel is None else self._channel_index(channel)
85
+ if frame is None:
86
+ return self.images[ci].sum(axis=0)
87
+ return self.images[ci, frame]
88
+
89
+ @property
90
+ def intensity_image(self):
91
+ return self.image()
92
+
93
+ def pixel_stack(self, *args, **kwargs):
94
+ raise ValueError('ISS .ifi is an intensity image with no lifetime data; use intensity_image / get_intensity_image()')
95
+
96
+ def summed_decay(self, *args, **kwargs):
97
+ raise ValueError('ISS .ifi is an intensity image with no lifetime data; use intensity_image / get_intensity_image()')
98
+
99
+ def read_ifi(path, channel=None, verbose=False):
100
+ iss = ISSImage(path, verbose=verbose)
101
+ img = iss.image(channel=channel)
102
+ metadata = {
103
+ 'shape': iss.images.shape,
104
+ 'dims': ('C', 'T', 'Y', 'X'),
105
+ 'n_x': iss.n_x,
106
+ 'n_y': iss.n_y,
107
+ 'n_channels': iss.n_channels,
108
+ 'n_frames': iss.n_frames,
109
+ 'tags': iss.tags,
110
+ }
111
+ return img, metadata
112
+
113
+ def get_intensity_image(path, channel=None):
114
+ return read_ifi(path, channel=channel, verbose=False)