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,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)
|