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,223 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
import numpy as np
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
_FRAME_CH = 5
|
|
6
|
+
_LINE_CH = 6
|
|
7
|
+
_PIXEL_CH = 7
|
|
8
|
+
_CLOCK_CH = 8
|
|
9
|
+
|
|
10
|
+
_TAG_EXTS = {
|
|
11
|
+
'time': '.TAGTIME',
|
|
12
|
+
'channel': '.TAGCHANNEL',
|
|
13
|
+
'decay': '.TAGDECAY',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
def _find_with_ext(stem, ext):
|
|
17
|
+
direct = Path(str(stem) + ext)
|
|
18
|
+
if direct.exists():
|
|
19
|
+
return direct
|
|
20
|
+
parent = stem.parent if str(stem.parent) else Path('.')
|
|
21
|
+
target = (stem.name + ext).lower()
|
|
22
|
+
if parent.exists():
|
|
23
|
+
for f in parent.iterdir():
|
|
24
|
+
if f.name.lower() == target:
|
|
25
|
+
return f
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
def _resolve_triplet(path):
|
|
29
|
+
p = Path(path)
|
|
30
|
+
stem = p.with_suffix('') if p.suffix.upper() in (e.upper() for e in _TAG_EXTS.values()) else p
|
|
31
|
+
out = {}
|
|
32
|
+
for key, ext in _TAG_EXTS.items():
|
|
33
|
+
cand = _find_with_ext(stem, ext)
|
|
34
|
+
if cand is None:
|
|
35
|
+
raise FileNotFoundError(f"Missing ISS TD-FLIM file '{stem.name}{ext}' next to {p}")
|
|
36
|
+
out[key] = cand
|
|
37
|
+
return out
|
|
38
|
+
|
|
39
|
+
def _read_tag_file(path, byteorder, value_bytes):
|
|
40
|
+
dtype = np.dtype(f'{byteorder}i{value_bytes}')
|
|
41
|
+
with open(path, 'rb') as fh:
|
|
42
|
+
period = struct.unpack(f'{byteorder}i', fh.read(4))[0]
|
|
43
|
+
data = np.frombuffer(fh.read(), dtype=dtype)
|
|
44
|
+
return period, data
|
|
45
|
+
|
|
46
|
+
class ISSFile:
|
|
47
|
+
def __init__(self, path, verbose=True, byteorder='<', n_bins=None):
|
|
48
|
+
self.path = str(path)
|
|
49
|
+
self.verbose = verbose
|
|
50
|
+
self.byteorder = byteorder
|
|
51
|
+
self.paths = _resolve_triplet(path)
|
|
52
|
+
self._total_photons = None
|
|
53
|
+
self._load()
|
|
54
|
+
self._parse_meta(n_bins=n_bins)
|
|
55
|
+
def _load(self):
|
|
56
|
+
p_t, self.arrival_ps = _read_tag_file(self.paths['time'], self.byteorder, 8)
|
|
57
|
+
p_c, self.channel = _read_tag_file(self.paths['channel'], self.byteorder, 4)
|
|
58
|
+
p_d, self.decay = _read_tag_file(self.paths['decay'], self.byteorder, 4)
|
|
59
|
+
periods = [p for p in (p_t, p_c, p_d) if p > 0]
|
|
60
|
+
self.period_ps = periods[0] if periods else 0
|
|
61
|
+
if len(set(periods)) > 1 and self.verbose:
|
|
62
|
+
print(f' WARNING: excitation period differs across files: {p_t}, {p_c}, {p_d} ps')
|
|
63
|
+
n = min(self.arrival_ps.shape[0], self.channel.shape[0], self.decay.shape[0])
|
|
64
|
+
if not (self.arrival_ps.shape[0] == self.channel.shape[0] == self.decay.shape[0]):
|
|
65
|
+
if self.verbose:
|
|
66
|
+
print(f' WARNING: event counts differ '
|
|
67
|
+
f'(time={self.arrival_ps.shape[0]}, channel={self.channel.shape[0]}, '
|
|
68
|
+
f'decay={self.decay.shape[0]}); truncating to {n}')
|
|
69
|
+
self.arrival_ps = self.arrival_ps[:n]
|
|
70
|
+
self.channel = self.channel[:n]
|
|
71
|
+
self.decay = self.decay[:n]
|
|
72
|
+
def _parse_meta(self, n_bins=None):
|
|
73
|
+
ach = np.abs(self.channel)
|
|
74
|
+
is_det = (ach >= 1) & (ach <= 4)
|
|
75
|
+
if n_bins is not None:
|
|
76
|
+
self.n_bins = int(n_bins)
|
|
77
|
+
else:
|
|
78
|
+
dec_ph = self.decay[is_det]
|
|
79
|
+
self.n_bins = int(dec_ph.max()) + 1 if dec_ph.size else 0
|
|
80
|
+
period_s = self.period_ps * 1e-12
|
|
81
|
+
self.frequency = (1.0 / period_s) if period_s > 0 else 0.0
|
|
82
|
+
self.period_ns = period_s * 1e9
|
|
83
|
+
self.tcspc_res = (period_s / self.n_bins) if self.n_bins > 0 else 0.0
|
|
84
|
+
self.time_ns = (np.arange(self.n_bins) + 0.5) * self.tcspc_res * 1e9
|
|
85
|
+
det = ach[is_det]
|
|
86
|
+
self.photon_channel = int(np.bincount(det, minlength=5)[1:5].argmax() + 1) if det.size else None
|
|
87
|
+
self.detector_channels = [int(c) for c in np.unique(det)] if det.size else []
|
|
88
|
+
self.sync_rate = self.frequency
|
|
89
|
+
self.n_records = int(self.channel.shape[0])
|
|
90
|
+
self.rec_type = None
|
|
91
|
+
is_frame = ach == _FRAME_CH
|
|
92
|
+
is_line = ach == _LINE_CH
|
|
93
|
+
is_pixel = ach == _PIXEL_CH
|
|
94
|
+
self.n_y = int(self._counts_since(is_frame, is_line).max()) if is_line.any() else 0
|
|
95
|
+
self.n_x = int(self._counts_since(is_line, is_pixel).max()) if is_pixel.any() else 0
|
|
96
|
+
self.tags = {
|
|
97
|
+
'ISS_ExcitationPeriod_ps': int(self.period_ps),
|
|
98
|
+
'ISS_NumEvents': int(self.channel.shape[0]),
|
|
99
|
+
'ISS_NumPhotons': int(is_det.sum()),
|
|
100
|
+
'ISS_DecayResolution': int(self.n_bins),
|
|
101
|
+
'ISS_DetectorChannels': self.detector_channels,
|
|
102
|
+
'ISS_ImageShape': (self.n_y, self.n_x),
|
|
103
|
+
'ISS_SourceFiles': {k: str(v) for k, v in self.paths.items()},
|
|
104
|
+
}
|
|
105
|
+
if self.verbose:
|
|
106
|
+
print(f" ISS TD-FLIM : {self.paths['time'].stem}")
|
|
107
|
+
print(f' Period : {self.period_ns:.3f} ns ({self.frequency/1e6:.3f} MHz)')
|
|
108
|
+
print(f' TCSPC : {self.n_bins} bins x {self.tcspc_res*1e12:.2f} ps')
|
|
109
|
+
print(f' Events : {self.channel.shape[0]:,} (photons {int(is_det.sum()):,})')
|
|
110
|
+
print(f' Channels : {self.detector_channels}')
|
|
111
|
+
print(' ')
|
|
112
|
+
def _counts_since(self, reset_mask, count_mask):
|
|
113
|
+
# counter that resets to 0 at every reset_mask event
|
|
114
|
+
n = reset_mask.shape[0]
|
|
115
|
+
cum = np.cumsum(count_mask.astype(np.int64))
|
|
116
|
+
reset_pos = np.where(reset_mask, np.arange(n), -1)
|
|
117
|
+
last = np.maximum.accumulate(reset_pos)
|
|
118
|
+
base = np.zeros(n, dtype=np.int64)
|
|
119
|
+
seen = last >= 0
|
|
120
|
+
base[seen] = cum[last[seen]]
|
|
121
|
+
return cum - base
|
|
122
|
+
def summed_decay(self, channel=None):
|
|
123
|
+
ach = np.abs(self.channel)
|
|
124
|
+
is_det = (ach >= 1) & (ach <= 4)
|
|
125
|
+
if channel is None:
|
|
126
|
+
channel = self.photon_channel
|
|
127
|
+
else:
|
|
128
|
+
channel = int(channel)
|
|
129
|
+
if channel is None:
|
|
130
|
+
return np.zeros(self.n_bins, dtype=float)
|
|
131
|
+
ph = is_det & (ach == channel)
|
|
132
|
+
dec = self.decay[ph]
|
|
133
|
+
dec = dec[(dec >= 0) & (dec < self.n_bins)].astype(np.int64)
|
|
134
|
+
decay = np.bincount(dec, minlength=self.n_bins).astype(float)
|
|
135
|
+
self._total_photons = int(decay.sum())
|
|
136
|
+
return decay[:self.n_bins]
|
|
137
|
+
n_sync = None
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def photons_per_pulse(self):
|
|
141
|
+
return None
|
|
142
|
+
def pixel_stack(self, channel=None, binning=1, n_x=None, n_y=None):
|
|
143
|
+
ach = np.abs(self.channel)
|
|
144
|
+
is_frame = ach == _FRAME_CH
|
|
145
|
+
is_line = ach == _LINE_CH
|
|
146
|
+
is_pixel = ach == _PIXEL_CH
|
|
147
|
+
is_det = (ach >= 1) & (ach <= 4)
|
|
148
|
+
if not is_line.any():
|
|
149
|
+
raise RuntimeError('No line markers (ch6) found; cannot build an image cube.')
|
|
150
|
+
# y/x are 0-based: line/pixel markers seen since the parent reset, minus one
|
|
151
|
+
y = self._counts_since(is_frame, is_line) - 1
|
|
152
|
+
x = self._counts_since(is_line, is_pixel) - 1
|
|
153
|
+
if channel is not None:
|
|
154
|
+
ph = is_det & (ach == int(channel))
|
|
155
|
+
else:
|
|
156
|
+
ph = is_det
|
|
157
|
+
dec = self.decay
|
|
158
|
+
valid = ph & (y >= 0) & (x >= 0) & (dec >= 0) & (dec < self.n_bins)
|
|
159
|
+
yv = y[valid].astype(np.int64)
|
|
160
|
+
xv = x[valid].astype(np.int64)
|
|
161
|
+
dv = dec[valid].astype(np.int64)
|
|
162
|
+
ny = int(n_y) if n_y is not None else (self.n_y or (int(yv.max()) + 1 if yv.size else 0))
|
|
163
|
+
nx = int(n_x) if n_x is not None else (self.n_x or (int(xv.max()) + 1 if xv.size else 0))
|
|
164
|
+
keep = (yv < ny) & (xv < nx)
|
|
165
|
+
yv, xv, dv = yv[keep], xv[keep], dv[keep]
|
|
166
|
+
if binning > 1:
|
|
167
|
+
yv = yv // binning
|
|
168
|
+
xv = xv // binning
|
|
169
|
+
ny = (ny + binning - 1) // binning
|
|
170
|
+
nx = (nx + binning - 1) // binning
|
|
171
|
+
if ny == 0 or nx == 0 or self.n_bins == 0:
|
|
172
|
+
return np.zeros((ny, nx, self.n_bins), dtype=np.uint32)
|
|
173
|
+
flat = (yv * nx + xv) * self.n_bins + dv
|
|
174
|
+
cube = np.bincount(flat, minlength=ny * nx * self.n_bins)
|
|
175
|
+
cube = cube.reshape(ny, nx, self.n_bins).astype(np.uint32)
|
|
176
|
+
self._total_photons = int(cube.sum())
|
|
177
|
+
if self.verbose:
|
|
178
|
+
print(f' Built cube {cube.shape} ({self._total_photons:,} photons)')
|
|
179
|
+
return cube
|
|
180
|
+
def raw_pixel_stack(self, channel=None, binning=1):
|
|
181
|
+
return self.pixel_stack(channel=channel, binning=binning)
|
|
182
|
+
|
|
183
|
+
def read_iss(path, binning=1, channel=None, verbose=False):
|
|
184
|
+
iss = ISSFile(path, verbose=verbose)
|
|
185
|
+
data = iss.pixel_stack(channel=channel, binning=binning)
|
|
186
|
+
metadata = {
|
|
187
|
+
'frequency': iss.frequency,
|
|
188
|
+
'tcspc_resolution': iss.tcspc_res,
|
|
189
|
+
'shape': data.shape,
|
|
190
|
+
'dims': ('Y', 'X', 'H'),
|
|
191
|
+
'tags': iss.tags,
|
|
192
|
+
'x_pixel_size': 0,
|
|
193
|
+
'y_pixel_size': 0,
|
|
194
|
+
'n_bins': iss.n_bins,
|
|
195
|
+
'time_ns': iss.time_ns,
|
|
196
|
+
'photon_channel': iss.photon_channel,
|
|
197
|
+
}
|
|
198
|
+
return data, metadata
|
|
199
|
+
|
|
200
|
+
def get_intensity_image(path, binning=1, channel=None):
|
|
201
|
+
data, metadata = read_iss(path, binning=binning, channel=channel, verbose=False)
|
|
202
|
+
img = data.sum(axis=2)
|
|
203
|
+
return img, metadata
|
|
204
|
+
|
|
205
|
+
def get_flim_data(path, binning=1, channel=None):
|
|
206
|
+
return read_iss(path, binning=binning, channel=channel, verbose=False)
|
|
207
|
+
|
|
208
|
+
def normalise_flim(flim):
|
|
209
|
+
if flim is None:
|
|
210
|
+
return None
|
|
211
|
+
if flim.ndim == 5:
|
|
212
|
+
return flim[0, :, :, 0, :]
|
|
213
|
+
if flim.ndim == 4:
|
|
214
|
+
if flim.shape[0] == 1:
|
|
215
|
+
return flim[0]
|
|
216
|
+
else:
|
|
217
|
+
return flim[:, :, 0, :]
|
|
218
|
+
if flim.ndim == 3:
|
|
219
|
+
return flim
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
def create_time_axis(n_bins, tcspc_resolution):
|
|
223
|
+
return np.arange(n_bins) * tcspc_resolution * 1e9
|
|
File without changes
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from photonsfile import read_header, read_attributes, has_dual_tdc, read_photons
|
|
4
|
+
|
|
5
|
+
class PSFile:
|
|
6
|
+
def __init__(self, path, verbose=True, channel=None, pixels=512,
|
|
7
|
+
n_bins=256, period_ns=None):
|
|
8
|
+
self.path = str(path)
|
|
9
|
+
self.verbose = verbose
|
|
10
|
+
self.header = read_header(self.path)
|
|
11
|
+
self.attrs = read_attributes(self.path)
|
|
12
|
+
self._names = [d['name'] for d in self.header['datasets']]
|
|
13
|
+
self.dual_tdc = has_dual_tdc(self.path)
|
|
14
|
+
self.pos_range = 1 << int(self.attrs.get('/photons/PositionBits', 12))
|
|
15
|
+
self.tac_range = 1 << int(self.attrs.get('/photons/TacBits', 12))
|
|
16
|
+
self.pixels = int(pixels)
|
|
17
|
+
self.n_bins = int(n_bins)
|
|
18
|
+
self.n_x = self.pixels
|
|
19
|
+
self.n_y = self.pixels
|
|
20
|
+
self._period_ns = period_ns
|
|
21
|
+
self._streams = None
|
|
22
|
+
self._total_photons = None
|
|
23
|
+
self.n_records = 0
|
|
24
|
+
self.n_channels = 1
|
|
25
|
+
self.photon_channel = 1
|
|
26
|
+
self.detector_channels = [1]
|
|
27
|
+
self._parse_meta(period_ns)
|
|
28
|
+
|
|
29
|
+
def _parse_meta(self, period_ns, announce=True):
|
|
30
|
+
# TacChannel = picoseconds per raw dt unit; dual-TDC dt is already in ps
|
|
31
|
+
tac_channel = self.attrs.get('/photons/TacChannel')
|
|
32
|
+
if period_ns and float(period_ns) > 0:
|
|
33
|
+
period_s = float(period_ns) * 1e-9
|
|
34
|
+
self.calib_source = 'user'
|
|
35
|
+
elif self.dual_tdc:
|
|
36
|
+
period_s = self.tac_range * 1e-12
|
|
37
|
+
self.calib_source = 'dual_tdc'
|
|
38
|
+
elif tac_channel:
|
|
39
|
+
period_s = self.tac_range * float(tac_channel) * 1e-12
|
|
40
|
+
self.calib_source = 'TacChannel'
|
|
41
|
+
else:
|
|
42
|
+
period_s = 0.0
|
|
43
|
+
self.calib_source = 'unknown'
|
|
44
|
+
if period_s > 0:
|
|
45
|
+
self.frequency = 1.0 / period_s
|
|
46
|
+
self.period_ns = period_s * 1e9
|
|
47
|
+
self.tcspc_res = period_s / self.n_bins
|
|
48
|
+
else:
|
|
49
|
+
self.frequency = 0.0
|
|
50
|
+
self.period_ns = 0.0
|
|
51
|
+
self.tcspc_res = 0.0
|
|
52
|
+
self.sync_rate = self.frequency
|
|
53
|
+
self.time_ns = (np.arange(self.n_bins) + 0.5) * self.tcspc_res * 1e9
|
|
54
|
+
self.dt_unit = 'ps' if self.dual_tdc else 'tac_bin'
|
|
55
|
+
self.tags = {
|
|
56
|
+
'PS_Magic': self.header['magic'],
|
|
57
|
+
'PS_Version': self.header['version'],
|
|
58
|
+
'PS_Datasets': self._names,
|
|
59
|
+
'PS_PositionRange': self.pos_range,
|
|
60
|
+
'PS_TacRange': self.tac_range,
|
|
61
|
+
'PS_TacBias': self.attrs.get('/photons/TacBias'),
|
|
62
|
+
'PS_TacChannel': self.attrs.get('/photons/TacChannel'),
|
|
63
|
+
'PS_TimerFrequency': self.attrs.get('/photons/TimerFrequency'),
|
|
64
|
+
'PS_DetectorGuid': self.attrs.get('/photons/DetectorGuid'),
|
|
65
|
+
'PS_Created': self.attrs.get('Created'),
|
|
66
|
+
'PS_Pixels': self.pixels,
|
|
67
|
+
'PS_DecayResolution': self.n_bins,
|
|
68
|
+
'PS_CalibrationSource': self.calib_source,
|
|
69
|
+
'PS_DetectorChannels': self.detector_channels,
|
|
70
|
+
'PS_SourceFile': self.path,
|
|
71
|
+
}
|
|
72
|
+
if self.verbose and announce:
|
|
73
|
+
print(f' Photonscore : {Path(self.path).name}')
|
|
74
|
+
print(f' Container : {self.header["magic"]} v{self.header["version"]}')
|
|
75
|
+
print(f' Image : {self.pixels} x {self.pixels} px '
|
|
76
|
+
f'(from {self.pos_range} raw positions)')
|
|
77
|
+
print(f' TCSPC : {self.n_bins} bins over {self.tac_range} dt units '
|
|
78
|
+
f'({self.tcspc_res*1e12:.2f} ps/bin, {self.calib_source})')
|
|
79
|
+
if self.dual_tdc:
|
|
80
|
+
print(' Timing : dual-TDC, dt = stop - start in ps '
|
|
81
|
+
'(range set from data)')
|
|
82
|
+
if self.calib_source == 'unknown':
|
|
83
|
+
print(' NOTE : dt->time calibration unknown; '
|
|
84
|
+
'pass period_ns=<laser period ns> for a real time axis')
|
|
85
|
+
print(' ')
|
|
86
|
+
|
|
87
|
+
n_sync = None
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def photons_per_pulse(self):
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def _ensure_streams(self):
|
|
94
|
+
if self._streams is None:
|
|
95
|
+
self._streams = read_photons(self.path)
|
|
96
|
+
if self.dual_tdc and 'dt' in self._streams:
|
|
97
|
+
dt = np.asarray(self._streams['dt'])
|
|
98
|
+
dt = dt[dt >= 0]
|
|
99
|
+
if dt.size:
|
|
100
|
+
self.tac_range = int(dt.max()) + 1
|
|
101
|
+
self._parse_meta(self._period_ns, announce=False)
|
|
102
|
+
return self._streams
|
|
103
|
+
|
|
104
|
+
def _bin_positions(self, binning):
|
|
105
|
+
s = self._ensure_streams()
|
|
106
|
+
x = np.asarray(s['x']).astype(np.int64)
|
|
107
|
+
y = np.asarray(s['y']).astype(np.int64)
|
|
108
|
+
dt = np.asarray(s['dt']).astype(np.int64)
|
|
109
|
+
n = min(x.shape[0], y.shape[0], dt.shape[0])
|
|
110
|
+
x, y, dt = x[:n], y[:n], dt[:n]
|
|
111
|
+
valid = ((x >= 0) & (x < self.pos_range) & (y >= 0) & (y < self.pos_range)
|
|
112
|
+
& (dt >= 0) & (dt < self.tac_range))
|
|
113
|
+
x, y, dt = x[valid], y[valid], dt[valid]
|
|
114
|
+
p = self.pixels
|
|
115
|
+
xi = (x * p) // self.pos_range
|
|
116
|
+
yi = (y * p) // self.pos_range
|
|
117
|
+
di = (dt * self.n_bins) // self.tac_range
|
|
118
|
+
if binning > 1:
|
|
119
|
+
xi = xi // binning
|
|
120
|
+
yi = yi // binning
|
|
121
|
+
p = (p + binning - 1) // binning
|
|
122
|
+
return xi, yi, di, p
|
|
123
|
+
|
|
124
|
+
def pixel_stack(self, channel=None, binning=1):
|
|
125
|
+
xi, yi, di, p = self._bin_positions(binning)
|
|
126
|
+
b = self.n_bins
|
|
127
|
+
if p == 0 or b == 0:
|
|
128
|
+
return np.zeros((p, p, b), dtype=np.uint32)
|
|
129
|
+
flat = (yi * p + xi) * b + di
|
|
130
|
+
cube = np.bincount(flat, minlength=p * p * b).reshape(p, p, b).astype(np.uint32)
|
|
131
|
+
self.n_y, self.n_x = cube.shape[0], cube.shape[1]
|
|
132
|
+
self._total_photons = int(cube.sum())
|
|
133
|
+
self.n_records = self._total_photons
|
|
134
|
+
if self.verbose:
|
|
135
|
+
print(f' Built cube {cube.shape} ({self._total_photons:,} photons)')
|
|
136
|
+
return cube
|
|
137
|
+
|
|
138
|
+
def raw_pixel_stack(self, channel=None, binning=1):
|
|
139
|
+
return self.pixel_stack(channel=channel, binning=binning)
|
|
140
|
+
|
|
141
|
+
def summed_decay(self, channel=None):
|
|
142
|
+
s = self._ensure_streams()
|
|
143
|
+
dt = np.asarray(s['dt']).astype(np.int64)
|
|
144
|
+
dt = dt[(dt >= 0) & (dt < self.tac_range)]
|
|
145
|
+
di = (dt * self.n_bins) // self.tac_range
|
|
146
|
+
decay = np.bincount(di, minlength=self.n_bins).astype(float)
|
|
147
|
+
self._total_photons = int(decay.sum())
|
|
148
|
+
return decay[:self.n_bins]
|
|
149
|
+
|
|
150
|
+
def intensity_image(self, channel=None, binning=1):
|
|
151
|
+
xi, yi, _, p = self._bin_positions(binning)
|
|
152
|
+
if p == 0:
|
|
153
|
+
return np.zeros((0, 0), dtype=np.uint64)
|
|
154
|
+
flat = yi * p + xi
|
|
155
|
+
img = np.bincount(flat, minlength=p * p).reshape(p, p).astype(np.uint64)
|
|
156
|
+
return img
|
|
157
|
+
|
|
158
|
+
def _metadata(ps, data):
|
|
159
|
+
return {
|
|
160
|
+
'frequency': ps.frequency,
|
|
161
|
+
'tcspc_resolution': ps.tcspc_res,
|
|
162
|
+
'shape': data.shape,
|
|
163
|
+
'dims': ('Y', 'X', 'H'),
|
|
164
|
+
'tags': ps.tags,
|
|
165
|
+
'x_pixel_size': 0,
|
|
166
|
+
'y_pixel_size': 0,
|
|
167
|
+
'n_bins': ps.n_bins,
|
|
168
|
+
'time_ns': ps.time_ns,
|
|
169
|
+
'photon_channel': ps.photon_channel,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
def read_ps(path, binning=1, channel=None, verbose=False, pixels=512,
|
|
173
|
+
n_bins=256, period_ns=None):
|
|
174
|
+
ps = PSFile(path, verbose=verbose, pixels=pixels, n_bins=n_bins, period_ns=period_ns)
|
|
175
|
+
data = ps.pixel_stack(channel=channel, binning=binning)
|
|
176
|
+
metadata = _metadata(ps, data)
|
|
177
|
+
return data, metadata
|
|
178
|
+
|
|
179
|
+
def get_flim_data(path, binning=1, channel=None, pixels=512, n_bins=256, period_ns=None):
|
|
180
|
+
return read_ps(path, binning=binning, channel=channel, verbose=False,
|
|
181
|
+
pixels=pixels, n_bins=n_bins, period_ns=period_ns)
|
|
182
|
+
|
|
183
|
+
def get_intensity_image(path, binning=1, channel=None, pixels=512, n_bins=256):
|
|
184
|
+
ps = PSFile(path, verbose=False, pixels=pixels, n_bins=n_bins)
|
|
185
|
+
img = ps.intensity_image(channel=channel, binning=binning)
|
|
186
|
+
metadata = _metadata(ps, np.empty(0))
|
|
187
|
+
metadata['shape'] = (ps.n_y, ps.n_x, ps.n_bins)
|
|
188
|
+
return img, metadata
|
|
189
|
+
|
|
190
|
+
def normalise_flim(flim):
|
|
191
|
+
if flim is None:
|
|
192
|
+
return None
|
|
193
|
+
if flim.ndim == 5:
|
|
194
|
+
return flim[0, :, :, 0, :]
|
|
195
|
+
if flim.ndim == 4:
|
|
196
|
+
return flim[0] if flim.shape[0] == 1 else flim[:, :, 0, :]
|
|
197
|
+
if flim.ndim == 3:
|
|
198
|
+
return flim
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
def create_time_axis(n_bins, tcspc_resolution):
|
|
202
|
+
return np.arange(n_bins) * tcspc_resolution * 1e9
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .reader import PTUFile, normalise_flim
|
|
3
|
+
|
|
4
|
+
def create_time_axis(n_bins, tcspc_resolution):
|
|
5
|
+
return np.arange(n_bins) * tcspc_resolution * 1e9
|
|
6
|
+
|
|
7
|
+
def get_flim_histogram_from_ptufile(
|
|
8
|
+
ptu_path,
|
|
9
|
+
rotate_cw=True,
|
|
10
|
+
binning=1,
|
|
11
|
+
channel=None
|
|
12
|
+
):
|
|
13
|
+
ptu = PTUFile(str(ptu_path), verbose=False)
|
|
14
|
+
stack = ptu.raw_pixel_stack(channel=channel, binning=binning)
|
|
15
|
+
if stack.sum() == 0:
|
|
16
|
+
raise ValueError('PTU decoded to zero photons')
|
|
17
|
+
if rotate_cw:
|
|
18
|
+
stack = np.rot90(stack, k=-1, axes=(0, 1))
|
|
19
|
+
metadata = {
|
|
20
|
+
'tcspc_resolution': ptu.tcspc_res,
|
|
21
|
+
'n_time_bins': ptu.n_bins,
|
|
22
|
+
'tile_shape': (ptu.n_y // binning, ptu.n_x // binning),
|
|
23
|
+
'frequency': ptu.sync_rate,
|
|
24
|
+
'binning': binning,
|
|
25
|
+
'channel': channel,
|
|
26
|
+
}
|
|
27
|
+
return stack, metadata
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
def _load_ptufile():
|
|
5
|
+
try:
|
|
6
|
+
import ptufile
|
|
7
|
+
except ImportError:
|
|
8
|
+
raise ImportError('ptufile is required to read PicoQuant .phu files '
|
|
9
|
+
'(pip install ptufile)')
|
|
10
|
+
return ptufile
|
|
11
|
+
|
|
12
|
+
class PHUFile:
|
|
13
|
+
modality = 'time'
|
|
14
|
+
|
|
15
|
+
def __init__(self, path, verbose=True, channel=None, **kwargs):
|
|
16
|
+
self.path = str(path)
|
|
17
|
+
self.verbose = verbose
|
|
18
|
+
self.channel = channel
|
|
19
|
+
ptufile = _load_ptufile()
|
|
20
|
+
with ptufile.PhuFile(self.path) as f:
|
|
21
|
+
self._hist = [np.asarray(h, dtype=np.uint32) for h in f.histograms()]
|
|
22
|
+
self.tcspc_res = float(f.tcspc_resolution)
|
|
23
|
+
self._resolutions = [float(r) for r in f.histogram_resolutions]
|
|
24
|
+
self.tags = dict(f.tags)
|
|
25
|
+
self.n_channels = len(self._hist)
|
|
26
|
+
self.n_bins = int(max((h.size for h in self._hist), default=0))
|
|
27
|
+
self.n_x = 0
|
|
28
|
+
self.n_y = 0
|
|
29
|
+
self.is_image = False
|
|
30
|
+
self.time_ns = (np.arange(self.n_bins) + 0.5) * self.tcspc_res * 1e9
|
|
31
|
+
self.sync_rate = 0.0
|
|
32
|
+
self.period_ns = 0.0
|
|
33
|
+
self.photon_channel = None
|
|
34
|
+
self._total_photons = None
|
|
35
|
+
if self.verbose:
|
|
36
|
+
print(f' PicoQuant PHU : {Path(self.path).name}')
|
|
37
|
+
print(f' TCSPC : {self.n_bins} bins x {self.tcspc_res*1e12:.2f} ps')
|
|
38
|
+
print(f' Point : no image (histogram mode), {self.n_channels} histogram(s)')
|
|
39
|
+
print(' ')
|
|
40
|
+
|
|
41
|
+
def _ch_index(self, channel):
|
|
42
|
+
if channel is None:
|
|
43
|
+
totals = [int(h.sum()) for h in self._hist]
|
|
44
|
+
idx = int(np.argmax(totals)) if totals else 0
|
|
45
|
+
self.photon_channel = idx + 1
|
|
46
|
+
return idx
|
|
47
|
+
c = int(channel)
|
|
48
|
+
idx = c - 1 if c >= 1 else c
|
|
49
|
+
return idx if 0 <= idx < self.n_channels else 0
|
|
50
|
+
|
|
51
|
+
def summed_decay(self, channel=None):
|
|
52
|
+
if not self._hist:
|
|
53
|
+
return np.zeros(self.n_bins, dtype=float)
|
|
54
|
+
idx = self._ch_index(self.channel if channel is None else channel)
|
|
55
|
+
decay = np.zeros(self.n_bins, dtype=float)
|
|
56
|
+
h = self._hist[idx]
|
|
57
|
+
decay[:h.size] = h
|
|
58
|
+
self._total_photons = int(decay.sum())
|
|
59
|
+
return decay
|
|
60
|
+
|
|
61
|
+
def pixel_stack(self, *args, **kwargs):
|
|
62
|
+
raise RuntimeError('PicoQuant .phu is histogram mode and has no scan image; '
|
|
63
|
+
'use summed_decay')
|
|
64
|
+
|
|
65
|
+
def raw_pixel_stack(self, *args, **kwargs):
|
|
66
|
+
return self.pixel_stack()
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def photons_per_pulse(self):
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def read_phu(path, channel=None, verbose=False):
|
|
73
|
+
phu = PHUFile(path, verbose=verbose)
|
|
74
|
+
decay = phu.summed_decay(channel=channel)
|
|
75
|
+
metadata = {
|
|
76
|
+
'frequency': phu.sync_rate,
|
|
77
|
+
'tcspc_resolution': phu.tcspc_res,
|
|
78
|
+
'shape': decay.shape,
|
|
79
|
+
'dims': ('H',),
|
|
80
|
+
'tags': phu.tags,
|
|
81
|
+
'n_bins': phu.n_bins,
|
|
82
|
+
'time_ns': phu.time_ns,
|
|
83
|
+
'photon_channel': phu.photon_channel,
|
|
84
|
+
}
|
|
85
|
+
return decay, metadata
|