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,94 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .reader import PTUFile
|
|
3
|
+
|
|
4
|
+
def signal_from_PTUFile(
|
|
5
|
+
filename,
|
|
6
|
+
/,
|
|
7
|
+
*,
|
|
8
|
+
dtype=None,
|
|
9
|
+
frame=None,
|
|
10
|
+
channel=0,
|
|
11
|
+
dtime=0,
|
|
12
|
+
binning=1,
|
|
13
|
+
keepdims=False,
|
|
14
|
+
):
|
|
15
|
+
from xarray import DataArray
|
|
16
|
+
|
|
17
|
+
ptu = PTUFile(str(filename), verbose=False)
|
|
18
|
+
is_image = ptu.n_x > 0 and ptu.n_y > 0
|
|
19
|
+
_active_channels = np.asarray(ptu._active)
|
|
20
|
+
|
|
21
|
+
if is_image:
|
|
22
|
+
cubes = [ptu.raw_pixel_stack(channel=int(c), binning=binning)
|
|
23
|
+
for c in _active_channels]
|
|
24
|
+
stack = np.stack(cubes, axis=0)
|
|
25
|
+
data = np.transpose(stack, (1, 2, 0, 3))[np.newaxis, ...]
|
|
26
|
+
else:
|
|
27
|
+
hists = [ptu.summed_decay(channel=int(c)) for c in _active_channels]
|
|
28
|
+
data = np.stack(hists, axis=0)[np.newaxis, np.newaxis, np.newaxis, :, :]
|
|
29
|
+
|
|
30
|
+
if dtype is None:
|
|
31
|
+
dtype = np.uint16
|
|
32
|
+
data = data.astype(dtype)
|
|
33
|
+
|
|
34
|
+
n_bins_full = data.shape[-1]
|
|
35
|
+
|
|
36
|
+
if dtime is not None:
|
|
37
|
+
if dtime < 0:
|
|
38
|
+
data = data.sum(axis=-1, keepdims=keepdims)
|
|
39
|
+
elif dtime > 0:
|
|
40
|
+
data = data[..., :dtime]
|
|
41
|
+
|
|
42
|
+
if channel is not None:
|
|
43
|
+
if channel < 0:
|
|
44
|
+
data = data.sum(axis=3, keepdims=keepdims)
|
|
45
|
+
else:
|
|
46
|
+
ch_positions = np.where(_active_channels == channel)[0]
|
|
47
|
+
if len(ch_positions) == 0:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"Channel {channel} not found in PTU file. "
|
|
50
|
+
f"Available hardware channels: {_active_channels.tolist()}"
|
|
51
|
+
)
|
|
52
|
+
channel_idx = int(ch_positions[0])
|
|
53
|
+
if keepdims:
|
|
54
|
+
data = data[:, :, :, channel_idx : channel_idx + 1]
|
|
55
|
+
else:
|
|
56
|
+
data = data[:, :, :, channel_idx]
|
|
57
|
+
|
|
58
|
+
if frame is not None:
|
|
59
|
+
if frame < 0:
|
|
60
|
+
data = data.sum(axis=0, keepdims=keepdims)
|
|
61
|
+
else:
|
|
62
|
+
if keepdims:
|
|
63
|
+
data = data[frame : frame + 1]
|
|
64
|
+
else:
|
|
65
|
+
data = data[frame]
|
|
66
|
+
|
|
67
|
+
all_dims = ['T', 'Y', 'X', 'C', 'H']
|
|
68
|
+
removed = []
|
|
69
|
+
if dtime is not None and dtime < 0 and not keepdims:
|
|
70
|
+
removed.append('H')
|
|
71
|
+
if channel is not None and channel >= 0 and not keepdims:
|
|
72
|
+
removed.append('C')
|
|
73
|
+
if frame is not None and not keepdims:
|
|
74
|
+
removed.append('T')
|
|
75
|
+
dims = [d for d in all_dims if d not in removed]
|
|
76
|
+
|
|
77
|
+
coords = {}
|
|
78
|
+
h_bins = dtime if (dtime is not None and dtime > 0) else n_bins_full
|
|
79
|
+
time_ns = ptu.time_ns[:h_bins]
|
|
80
|
+
if 'H' in dims:
|
|
81
|
+
coords['H'] = ('H', time_ns)
|
|
82
|
+
|
|
83
|
+
frequency_mhz = ptu.sync_rate * 1e-6
|
|
84
|
+
|
|
85
|
+
da = DataArray(
|
|
86
|
+
data,
|
|
87
|
+
dims=dims,
|
|
88
|
+
coords=coords,
|
|
89
|
+
attrs={
|
|
90
|
+
'frequency': frequency_mhz,
|
|
91
|
+
'ptu_tags': ptu.tags,
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
return da
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import importlib
|
|
3
|
+
|
|
4
|
+
_FORMATS = [
|
|
5
|
+
{'id': 'ptu', 'label': 'PicoQuant PTU', 'exts': ('.ptu',),
|
|
6
|
+
'modality': 'time', 'reader': 'flimkit.formats.PTU.reader:PTUFile'},
|
|
7
|
+
{'id': 'bh_sdt', 'label': 'Becker & Hickl SDT', 'exts': ('.sdt',),
|
|
8
|
+
'modality': 'time', 'reader': 'flimkit.formats.BH.reader:BHFile'},
|
|
9
|
+
{'id': 'iss_tdflim', 'label': 'ISS time-tag',
|
|
10
|
+
'exts': ('.tagtime', '.tagchannel', '.tagdecay'),
|
|
11
|
+
'modality': 'time', 'reader': 'flimkit.formats.ISS.reader:ISSFile'},
|
|
12
|
+
{'id': 'iss_image', 'label': 'ISS image', 'exts': ('.ifi',),
|
|
13
|
+
'modality': 'intensity', 'reader': 'flimkit.formats.ISS.image:ISSImage'},
|
|
14
|
+
{'id': 'iss_fdflim', 'label': 'ISS FD-FLIM (phasor)', 'exts': ('.ifli',),
|
|
15
|
+
'modality': 'frequency', 'reader': 'flimkit.formats.ISS.fdflim:ISSFdFlim'},
|
|
16
|
+
{'id': 'ps', 'label': 'Photonscore .photons', 'exts': ('.photons',),
|
|
17
|
+
'modality': 'time', 'reader': 'flimkit.formats.PS.reader:PSFile'},
|
|
18
|
+
{'id': 'pq_phu', 'label': 'PicoQuant PHU (histogram)', 'exts': ('.phu',),
|
|
19
|
+
'modality': 'time', 'reader': 'flimkit.formats.PTU.phu:PHUFile'},
|
|
20
|
+
{'id': 'pq_bin', 'label': 'PicoQuant BIN', 'exts': ('.bin',),
|
|
21
|
+
'modality': 'time', 'reader': 'flimkit.formats.signal:PQBinFile'},
|
|
22
|
+
{'id': 'simfcs_bh', 'label': 'SimFCS B&H', 'exts': ('.b&h',),
|
|
23
|
+
'modality': 'time', 'reader': 'flimkit.formats.signal:SimfcsBHFile'},
|
|
24
|
+
{'id': 'simfcs_bhz', 'label': 'SimFCS BHZ', 'exts': ('.bhz',),
|
|
25
|
+
'modality': 'time', 'reader': 'flimkit.formats.signal:SimfcsBHZFile'},
|
|
26
|
+
{'id': 'imspector_tiff', 'label': 'ImSpector FLIM TIFF', 'exts': ('.tif', '.tiff'),
|
|
27
|
+
'modality': 'time', 'reader': 'flimkit.formats.signal:ImspectorTIFFFile'},
|
|
28
|
+
{'id': 'iss_vista_tdflim', 'label': 'ISS Vista TDFLIM', 'exts': ('.iss-tdflim', '.tdflim'),
|
|
29
|
+
'modality': 'time', 'reader': 'flimkit.formats.signal:VistaTdflimFile'},
|
|
30
|
+
{'id': 'simfcs_referenced', 'label': 'SimFCS referenced (phasor)', 'exts': ('.ref', '.r64'),
|
|
31
|
+
'modality': 'frequency', 'reader': 'flimkit.formats.phasor:SimfcsReferencedFile'},
|
|
32
|
+
{'id': 'ometiff_phasor', 'label': 'PhasorPy OME-TIFF (phasor)', 'exts': ('.ome.tif',),
|
|
33
|
+
'modality': 'frequency', 'reader': 'flimkit.formats.phasor:OmeTiffPhasorFile'},
|
|
34
|
+
{'id': 'flimlabs_phasor', 'label': 'FLIM LABS phasor (JSON)', 'exts': ('.json',),
|
|
35
|
+
'modality': 'frequency', 'reader': 'flimkit.formats.phasor:FlimLabsPhasorFile'},
|
|
36
|
+
{'id': 'flimlabs_signal', 'label': 'FLIM LABS imaging (JSON)', 'exts': (),
|
|
37
|
+
'modality': 'time', 'reader': 'flimkit.formats.signal:FlimLabsSignalFile'},
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
_cache = {'version': None, 'formats': None, 'ext_to_id': None, 'modality_by_id': None}
|
|
41
|
+
|
|
42
|
+
def _registry():
|
|
43
|
+
from flimkit import plugins
|
|
44
|
+
plugins.ensure_loaded()
|
|
45
|
+
return plugins.registry
|
|
46
|
+
|
|
47
|
+
def _all_formats():
|
|
48
|
+
reg = _registry()
|
|
49
|
+
if _cache['version'] != reg.version():
|
|
50
|
+
merged = [dict(f) for f in _FORMATS]
|
|
51
|
+
known = {f['id'] for f in merged}
|
|
52
|
+
for f in reg.formats():
|
|
53
|
+
if f.id in known:
|
|
54
|
+
continue
|
|
55
|
+
merged.append({'id': f.id, 'label': f.label, 'exts': f.exts,
|
|
56
|
+
'modality': f.modality, 'reader': f})
|
|
57
|
+
ext_to_id = {}
|
|
58
|
+
for f in merged:
|
|
59
|
+
for ext in f['exts']:
|
|
60
|
+
ext_to_id.setdefault(ext, f['id'])
|
|
61
|
+
_cache['formats'] = merged
|
|
62
|
+
_cache['ext_to_id'] = ext_to_id
|
|
63
|
+
_cache['modality_by_id'] = {f['id']: f['modality'] for f in merged}
|
|
64
|
+
_cache['version'] = reg.version()
|
|
65
|
+
return _cache['formats']
|
|
66
|
+
|
|
67
|
+
def _ext_to_id():
|
|
68
|
+
_all_formats()
|
|
69
|
+
return _cache['ext_to_id']
|
|
70
|
+
|
|
71
|
+
def _modality_by_id():
|
|
72
|
+
_all_formats()
|
|
73
|
+
return _cache['modality_by_id']
|
|
74
|
+
|
|
75
|
+
def _find_sibling(stem, ext_lower):
|
|
76
|
+
direct = Path(str(stem) + ext_lower)
|
|
77
|
+
if direct.exists():
|
|
78
|
+
return direct
|
|
79
|
+
parent = stem.parent if str(stem.parent) else Path('.')
|
|
80
|
+
target = stem.name.lower() + ext_lower
|
|
81
|
+
if parent.exists():
|
|
82
|
+
for f in parent.iterdir():
|
|
83
|
+
if f.name.lower() == target:
|
|
84
|
+
return f
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def _has_iss_triplet(p):
|
|
88
|
+
stem = p.with_suffix('') if p.suffix else p
|
|
89
|
+
return _find_sibling(stem, '.tagtime') is not None
|
|
90
|
+
|
|
91
|
+
def _sniff_tiff(p):
|
|
92
|
+
try:
|
|
93
|
+
with open(p, 'rb') as fh:
|
|
94
|
+
head = fh.read(4)
|
|
95
|
+
except OSError:
|
|
96
|
+
return 'unknown'
|
|
97
|
+
if head not in (b'II*\x00', b'MM\x00*'):
|
|
98
|
+
return 'unknown'
|
|
99
|
+
try:
|
|
100
|
+
import tifffile
|
|
101
|
+
with tifffile.TiffFile(str(p)) as tif:
|
|
102
|
+
make = tif.pages.first.tags.valueof(271, '')
|
|
103
|
+
if make == 'ImSpector':
|
|
104
|
+
return 'imspector_tiff'
|
|
105
|
+
names = [s.name for s in tif.series[:3]]
|
|
106
|
+
if tif.is_ome and names == ['Phasor mean', 'Phasor real', 'Phasor imag']:
|
|
107
|
+
return 'ometiff_phasor'
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
return 'unknown'
|
|
111
|
+
|
|
112
|
+
def _scan_bytes(p, needle, chunk=1 << 20):
|
|
113
|
+
try:
|
|
114
|
+
with open(p, 'rb') as fh:
|
|
115
|
+
tail = b''
|
|
116
|
+
while True:
|
|
117
|
+
buf = fh.read(chunk)
|
|
118
|
+
if not buf:
|
|
119
|
+
return False
|
|
120
|
+
if needle in tail + buf:
|
|
121
|
+
return True
|
|
122
|
+
tail = buf[-len(needle):]
|
|
123
|
+
except OSError:
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
def _sniff_json(p):
|
|
127
|
+
try:
|
|
128
|
+
with open(p, 'rb') as fh:
|
|
129
|
+
head = fh.read(8192)
|
|
130
|
+
except OSError:
|
|
131
|
+
return 'unknown'
|
|
132
|
+
if b'"file_id"' not in head or b'"laser_period_ns"' not in head:
|
|
133
|
+
return 'unknown'
|
|
134
|
+
if _scan_bytes(p, b'"phasors_data"'):
|
|
135
|
+
return 'flimlabs_phasor'
|
|
136
|
+
if _scan_bytes(p, b'"intensities_data"') or _scan_bytes(p, b'"data"'):
|
|
137
|
+
return 'flimlabs_signal'
|
|
138
|
+
return 'unknown'
|
|
139
|
+
|
|
140
|
+
def _sniff_magic(p):
|
|
141
|
+
try:
|
|
142
|
+
with open(p, 'rb') as fh:
|
|
143
|
+
head = fh.read(64)
|
|
144
|
+
except OSError:
|
|
145
|
+
return 'unknown'
|
|
146
|
+
if b'PQHISTO' in head:
|
|
147
|
+
return 'pq_phu'
|
|
148
|
+
if b'PQTTTR' in head or b'PTU' in head:
|
|
149
|
+
return 'ptu'
|
|
150
|
+
if head[:12] == b'VistaFLImage':
|
|
151
|
+
return 'iss_fdflim'
|
|
152
|
+
if head[:10] == b'VISTAIMAGE':
|
|
153
|
+
return 'iss_image'
|
|
154
|
+
if b'D7 Photons Data' in head:
|
|
155
|
+
return 'ps'
|
|
156
|
+
return 'unknown'
|
|
157
|
+
|
|
158
|
+
def _clean_path(path):
|
|
159
|
+
s = str(path).strip()
|
|
160
|
+
if len(s) >= 2 and s[0] == s[-1] and s[0] in ('"', "'"):
|
|
161
|
+
s = s[1:-1].strip()
|
|
162
|
+
return s
|
|
163
|
+
|
|
164
|
+
def _run_sniffers(tier, p):
|
|
165
|
+
for sniffer in _registry().sniffers(tier):
|
|
166
|
+
try:
|
|
167
|
+
found = sniffer(p)
|
|
168
|
+
except Exception as exc:
|
|
169
|
+
print(f'[Plugins] sniffer from {sniffer.source} raised {exc}')
|
|
170
|
+
continue
|
|
171
|
+
if found and found != 'unknown':
|
|
172
|
+
return found
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def detect_format(path):
|
|
176
|
+
p = Path(_clean_path(path))
|
|
177
|
+
ext = p.suffix.lower()
|
|
178
|
+
if ext in ('.tif', '.tiff'):
|
|
179
|
+
return _sniff_tiff(p)
|
|
180
|
+
if ext == '.json':
|
|
181
|
+
return _sniff_json(p)
|
|
182
|
+
found = _run_sniffers('extension', p)
|
|
183
|
+
if found is not None:
|
|
184
|
+
return found
|
|
185
|
+
ext_to_id = _ext_to_id()
|
|
186
|
+
if ext in ext_to_id:
|
|
187
|
+
return ext_to_id[ext]
|
|
188
|
+
if _has_iss_triplet(p):
|
|
189
|
+
return 'iss_tdflim'
|
|
190
|
+
found = _sniff_magic(p)
|
|
191
|
+
if found != 'unknown':
|
|
192
|
+
return found
|
|
193
|
+
found = _run_sniffers('magic', p)
|
|
194
|
+
if found is not None:
|
|
195
|
+
return found
|
|
196
|
+
return 'unknown'
|
|
197
|
+
|
|
198
|
+
def file_modality(path):
|
|
199
|
+
return _modality_by_id().get(detect_format(path), 'unknown')
|
|
200
|
+
|
|
201
|
+
def supported_formats():
|
|
202
|
+
return [dict(f) for f in _all_formats()]
|
|
203
|
+
|
|
204
|
+
def supported_extensions():
|
|
205
|
+
return [ext for f in _all_formats() for ext in f['exts']]
|
|
206
|
+
|
|
207
|
+
def file_dialog_filetypes():
|
|
208
|
+
all_globs = ' '.join('*' + ext for ext in supported_extensions())
|
|
209
|
+
types = [('FLIM files', all_globs)]
|
|
210
|
+
for f in _all_formats():
|
|
211
|
+
globs = ' '.join('*' + ext for ext in f['exts'])
|
|
212
|
+
types.append((f['label'], globs))
|
|
213
|
+
return types
|
|
214
|
+
|
|
215
|
+
def _load_reader(fmt):
|
|
216
|
+
for f in _all_formats():
|
|
217
|
+
if f['id'] == fmt:
|
|
218
|
+
spec = f['reader']
|
|
219
|
+
if isinstance(spec, str):
|
|
220
|
+
mod_name, cls_name = spec.split(':')
|
|
221
|
+
return getattr(importlib.import_module(mod_name), cls_name)
|
|
222
|
+
return spec.reader()
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
class FLIMFile:
|
|
226
|
+
def __new__(cls, path, **kwargs):
|
|
227
|
+
path = _clean_path(path)
|
|
228
|
+
fmt = detect_format(path)
|
|
229
|
+
reader = _load_reader(fmt)
|
|
230
|
+
if reader is None:
|
|
231
|
+
raise ValueError(f'Unrecognised FLIM file format: {path}')
|
|
232
|
+
return reader(str(path), **kwargs)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
def _import_io():
|
|
5
|
+
try:
|
|
6
|
+
import phasorpy.io as pio
|
|
7
|
+
except ImportError:
|
|
8
|
+
raise ImportError('phasorpy is required to read this format '
|
|
9
|
+
'(pip install phasorpy)')
|
|
10
|
+
return pio
|
|
11
|
+
|
|
12
|
+
class PhasorReader:
|
|
13
|
+
modality = 'frequency'
|
|
14
|
+
|
|
15
|
+
def __init__(self, path, mean, real, imag, attrs, label='phasor',
|
|
16
|
+
verbose=True, channel=None, frequency_mhz=None):
|
|
17
|
+
self.path = str(path)
|
|
18
|
+
self.verbose = verbose
|
|
19
|
+
self.label = label
|
|
20
|
+
self.channel = channel
|
|
21
|
+
self._mean = np.asarray(mean)
|
|
22
|
+
self._real = np.asarray(real)
|
|
23
|
+
self._imag = np.asarray(imag)
|
|
24
|
+
attrs = dict(attrs or {})
|
|
25
|
+
self._dims = tuple(attrs.get('dims') or ())
|
|
26
|
+
self._harmonics = attrs.get('harmonic')
|
|
27
|
+
freq = frequency_mhz if frequency_mhz is not None else attrs.get('frequency')
|
|
28
|
+
self.frequency_mhz = float(freq) if freq else 0.0
|
|
29
|
+
self.tags = {f'phasorpy_{k}': v for k, v in attrs.items()}
|
|
30
|
+
dims = self._dims
|
|
31
|
+
self.n_channels = int(self._mean.shape[dims.index('C')]) if 'C' in dims else 1
|
|
32
|
+
self.n_y = int(self._mean.shape[dims.index('Y')]) if 'Y' in dims else 0
|
|
33
|
+
self.n_x = int(self._mean.shape[dims.index('X')]) if 'X' in dims else 0
|
|
34
|
+
self.is_image = self.n_x > 0 and self.n_y > 0
|
|
35
|
+
self.n_harmonics = len(self._harmonics) if isinstance(self._harmonics, (list, tuple)) else 1
|
|
36
|
+
if self.verbose:
|
|
37
|
+
print(f' {label} : {Path(self.path).name}')
|
|
38
|
+
if self.is_image:
|
|
39
|
+
print(f' Size : {self.n_x} x {self.n_y} px, {self.n_channels} ch, '
|
|
40
|
+
f'{self.n_harmonics} harmonic(s) dims={"".join(dims)}')
|
|
41
|
+
else:
|
|
42
|
+
print(f' Point : no image (no Y/X axes), {self.n_channels} ch, '
|
|
43
|
+
f'{self.n_harmonics} harmonic(s) dims={"".join(dims)}')
|
|
44
|
+
if self.frequency_mhz:
|
|
45
|
+
print(f' Frequency: {self.frequency_mhz:.3f} MHz')
|
|
46
|
+
else:
|
|
47
|
+
print(' WARNING : file carries no frequency; pass frequency_mhz=<MHz> '
|
|
48
|
+
'for the universal circle / lifetime overlay')
|
|
49
|
+
print(' Frequency-domain data: loads into phasor analysis, no lifetime fitting')
|
|
50
|
+
print(' ')
|
|
51
|
+
|
|
52
|
+
def _ch_index(self, channel):
|
|
53
|
+
if channel is None:
|
|
54
|
+
return None
|
|
55
|
+
c = int(channel)
|
|
56
|
+
idx = c - 1 if c >= 1 else c
|
|
57
|
+
return idx if 0 <= idx < self.n_channels else 0
|
|
58
|
+
|
|
59
|
+
def _auto_channel(self):
|
|
60
|
+
dims = list(self._dims)
|
|
61
|
+
if 'C' not in dims:
|
|
62
|
+
return 0
|
|
63
|
+
ci = dims.index('C')
|
|
64
|
+
sums = self._mean.sum(axis=tuple(i for i in range(self._mean.ndim) if i != ci))
|
|
65
|
+
return int(np.argmax(sums))
|
|
66
|
+
|
|
67
|
+
def _reduce(self, arr, dims, ch_idx):
|
|
68
|
+
dims = list(dims)
|
|
69
|
+
if arr.ndim != len(dims):
|
|
70
|
+
dims = dims[-arr.ndim:]
|
|
71
|
+
if 'C' in dims:
|
|
72
|
+
ci = dims.index('C')
|
|
73
|
+
arr = np.take(arr, ch_idx, axis=ci)
|
|
74
|
+
dims = [d for i, d in enumerate(dims) if i != ci]
|
|
75
|
+
while len(dims) > 2:
|
|
76
|
+
extra = next(i for i, d in enumerate(dims) if d not in ('Y', 'X'))
|
|
77
|
+
arr = np.take(arr, 0, axis=extra)
|
|
78
|
+
dims = [d for i, d in enumerate(dims) if i != extra]
|
|
79
|
+
order = [dims.index(d) for d in ('Y', 'X') if d in dims]
|
|
80
|
+
return np.transpose(arr, order) if len(order) == 2 else arr
|
|
81
|
+
|
|
82
|
+
def phasor(self, channel=None, harmonic=0, calibrate=True, **kwargs):
|
|
83
|
+
ch = self.channel if channel is None else channel
|
|
84
|
+
ch_idx = self._ch_index(ch)
|
|
85
|
+
if ch_idx is None:
|
|
86
|
+
ch_idx = self._auto_channel()
|
|
87
|
+
real, imag = self._real, self._imag
|
|
88
|
+
if isinstance(self._harmonics, (list, tuple)):
|
|
89
|
+
hi = int(harmonic) if 0 <= int(harmonic) < len(self._harmonics) else 0
|
|
90
|
+
real, imag = real[hi], imag[hi]
|
|
91
|
+
mean2 = self._reduce(self._mean, self._dims, ch_idx)
|
|
92
|
+
real2 = self._reduce(real, self._dims, ch_idx)
|
|
93
|
+
imag2 = self._reduce(imag, self._dims, ch_idx)
|
|
94
|
+
return (mean2.astype(float), real2.astype(float), imag2.astype(float),
|
|
95
|
+
self.frequency_mhz)
|
|
96
|
+
|
|
97
|
+
def pixel_stack(self, *args, **kwargs):
|
|
98
|
+
raise ValueError(f'{self.label} is frequency-domain phasor data: there is no '
|
|
99
|
+
'time-domain decay to fit. Load it into phasor analysis instead.')
|
|
100
|
+
|
|
101
|
+
def summed_decay(self, *args, **kwargs):
|
|
102
|
+
return self.pixel_stack()
|
|
103
|
+
|
|
104
|
+
def raw_pixel_stack(self, *args, **kwargs):
|
|
105
|
+
return self.pixel_stack()
|
|
106
|
+
|
|
107
|
+
class _GohlkePhasorFile(PhasorReader):
|
|
108
|
+
_reader_name = None
|
|
109
|
+
_label = None
|
|
110
|
+
_reader_kwargs = {}
|
|
111
|
+
|
|
112
|
+
def __init__(self, path, verbose=True, channel=None, **kwargs):
|
|
113
|
+
pio = _import_io()
|
|
114
|
+
fn = getattr(pio, self._reader_name, None)
|
|
115
|
+
if fn is None:
|
|
116
|
+
raise ImportError(f'{self._reader_name} is not available in this '
|
|
117
|
+
f'phasorpy version; upgrade phasorpy')
|
|
118
|
+
mean, real, imag, attrs = fn(str(path), **self._reader_kwargs)
|
|
119
|
+
super().__init__(path, mean, real, imag, attrs, label=self._label,
|
|
120
|
+
verbose=verbose, channel=channel, **kwargs)
|
|
121
|
+
|
|
122
|
+
class SimfcsReferencedFile(_GohlkePhasorFile):
|
|
123
|
+
_reader_name = 'phasor_from_simfcs_referenced'
|
|
124
|
+
_label = 'SimFCS referenced'
|
|
125
|
+
|
|
126
|
+
class OmeTiffPhasorFile(_GohlkePhasorFile):
|
|
127
|
+
_reader_name = 'phasor_from_ometiff'
|
|
128
|
+
_label = 'PhasorPy OME-TIFF'
|
|
129
|
+
|
|
130
|
+
class FlimLabsPhasorFile(_GohlkePhasorFile):
|
|
131
|
+
_reader_name = 'phasor_from_flimlabs_json'
|
|
132
|
+
_label = 'FLIM LABS phasor'
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
def _import_io():
|
|
5
|
+
try:
|
|
6
|
+
import phasorpy.io as pio
|
|
7
|
+
except ImportError:
|
|
8
|
+
raise ImportError('phasorpy is required to read this format '
|
|
9
|
+
'(pip install phasorpy)')
|
|
10
|
+
return pio
|
|
11
|
+
|
|
12
|
+
def _bin_cube(cube, binning):
|
|
13
|
+
if binning <= 1:
|
|
14
|
+
return cube
|
|
15
|
+
ny, nx, nh = cube.shape
|
|
16
|
+
ny2, nx2 = (ny // binning) * binning, (nx // binning) * binning
|
|
17
|
+
cube = cube[:ny2, :nx2, :]
|
|
18
|
+
return cube.reshape(ny2 // binning, binning, nx2 // binning, binning, nh).sum(axis=(1, 3))
|
|
19
|
+
|
|
20
|
+
class SignalReader:
|
|
21
|
+
modality = 'time'
|
|
22
|
+
|
|
23
|
+
def __init__(self, path, signal, label='signal', verbose=True, channel=None,
|
|
24
|
+
bin_width_ns=None, frequency_mhz=None):
|
|
25
|
+
self.path = str(path)
|
|
26
|
+
self.verbose = verbose
|
|
27
|
+
self.label = label
|
|
28
|
+
self.channel = channel
|
|
29
|
+
dims = list(signal.dims)
|
|
30
|
+
vals = np.asarray(signal.values)
|
|
31
|
+
attrs = dict(getattr(signal, 'attrs', {}) or {})
|
|
32
|
+
coords = getattr(signal, 'coords', {})
|
|
33
|
+
self._dims = dims
|
|
34
|
+
self._vals = vals
|
|
35
|
+
self.tags = {f'phasorpy_{k}': v for k, v in attrs.items()}
|
|
36
|
+
c_ax = dims.index('C') if 'C' in dims else None
|
|
37
|
+
self.n_channels = int(vals.shape[c_ax]) if c_ax is not None else 1
|
|
38
|
+
self._c_ax = c_ax
|
|
39
|
+
h_ax = dims.index('H') if 'H' in dims else None
|
|
40
|
+
if h_ax is None:
|
|
41
|
+
raise ValueError(f'{label}: no TCSPC histogram (H) axis in {dims}')
|
|
42
|
+
self.n_bins = int(vals.shape[h_ax])
|
|
43
|
+
self.n_y = int(vals.shape[dims.index('Y')]) if 'Y' in dims else 0
|
|
44
|
+
self.n_x = int(vals.shape[dims.index('X')]) if 'X' in dims else 0
|
|
45
|
+
self.is_image = self.n_x > 0 and self.n_y > 0
|
|
46
|
+
h_ns = None
|
|
47
|
+
if 'H' in coords:
|
|
48
|
+
h_ns = np.asarray(coords['H'].values, dtype=float)
|
|
49
|
+
elif bin_width_ns is not None:
|
|
50
|
+
h_ns = np.arange(self.n_bins) * float(bin_width_ns)
|
|
51
|
+
if h_ns is not None and h_ns.size > 1:
|
|
52
|
+
self.tcspc_res = float(np.median(np.diff(h_ns))) * 1e-9
|
|
53
|
+
self.time_ns = h_ns
|
|
54
|
+
else:
|
|
55
|
+
self.tcspc_res = float(bin_width_ns) * 1e-9 if bin_width_ns else 0.0
|
|
56
|
+
self.time_ns = np.arange(self.n_bins) * (self.tcspc_res * 1e9)
|
|
57
|
+
freq_mhz = frequency_mhz if frequency_mhz is not None else attrs.get('frequency')
|
|
58
|
+
self.sync_rate = float(freq_mhz) * 1e6 if freq_mhz else 0.0
|
|
59
|
+
self.period_ns = (1e9 / self.sync_rate) if self.sync_rate else 0.0
|
|
60
|
+
self.photon_channel = None
|
|
61
|
+
self._total_photons = None
|
|
62
|
+
self._cube_cache = {}
|
|
63
|
+
if self.verbose:
|
|
64
|
+
print(f' {label} : {Path(self.path).name}')
|
|
65
|
+
print(f' TCSPC : {self.n_bins} bins x {self.tcspc_res*1e12:.2f} ps')
|
|
66
|
+
if self.sync_rate:
|
|
67
|
+
print(f' Laser : {self.sync_rate/1e6:.3f} MHz ({self.period_ns:.3f} ns)')
|
|
68
|
+
if self.is_image:
|
|
69
|
+
print(f' Image : {self.n_x} x {self.n_y} px, {self.n_channels} channel(s)')
|
|
70
|
+
else:
|
|
71
|
+
print(f' Point : no image (no Y/X axes), {self.n_channels} channel(s)')
|
|
72
|
+
if self.tcspc_res <= 0:
|
|
73
|
+
print(f' WARNING : {label} carries no time axis; pass bin_width_ns='
|
|
74
|
+
f'<ns> (and frequency_mhz=<MHz>) or fits will be meaningless')
|
|
75
|
+
print(' ')
|
|
76
|
+
|
|
77
|
+
def _ch_index(self, channel):
|
|
78
|
+
if channel is None:
|
|
79
|
+
return None
|
|
80
|
+
c = int(channel)
|
|
81
|
+
idx = c - 1 if c >= 1 else c
|
|
82
|
+
return idx if 0 <= idx < self.n_channels else 0
|
|
83
|
+
|
|
84
|
+
def _cube_for(self, channel):
|
|
85
|
+
key = channel
|
|
86
|
+
if key in self._cube_cache:
|
|
87
|
+
return self._cube_cache[key]
|
|
88
|
+
arr = self._vals
|
|
89
|
+
dims = list(self._dims)
|
|
90
|
+
if self._c_ax is not None:
|
|
91
|
+
idx = self._ch_index(channel)
|
|
92
|
+
if idx is None:
|
|
93
|
+
sums = arr.sum(axis=tuple(i for i in range(arr.ndim) if i != self._c_ax))
|
|
94
|
+
idx = int(np.argmax(sums))
|
|
95
|
+
self.photon_channel = idx + 1
|
|
96
|
+
arr = np.take(arr, idx, axis=self._c_ax)
|
|
97
|
+
dims = [d for i, d in enumerate(dims) if i != self._c_ax]
|
|
98
|
+
drop = tuple(i for i, d in enumerate(dims) if d not in ('Y', 'X', 'H'))
|
|
99
|
+
if drop:
|
|
100
|
+
arr = arr.sum(axis=drop)
|
|
101
|
+
dims = [d for d in dims if d in ('Y', 'X', 'H')]
|
|
102
|
+
order = [dims.index(d) for d in ('Y', 'X', 'H') if d in dims]
|
|
103
|
+
cube = np.transpose(arr, order)
|
|
104
|
+
self._cube_cache[key] = cube
|
|
105
|
+
return cube
|
|
106
|
+
|
|
107
|
+
def summed_decay(self, channel=None):
|
|
108
|
+
cube = self._cube_for(self.channel if channel is None else channel)
|
|
109
|
+
decay = cube.reshape(-1, self.n_bins).sum(axis=0).astype(float)
|
|
110
|
+
self._total_photons = int(decay.sum())
|
|
111
|
+
return decay
|
|
112
|
+
|
|
113
|
+
def raw_pixel_stack(self, channel=None, binning=1):
|
|
114
|
+
cube = self._cube_for(self.channel if channel is None else channel)
|
|
115
|
+
if cube.ndim != 3:
|
|
116
|
+
raise RuntimeError(f'{self.label} has no scan image; use summed_decay')
|
|
117
|
+
if binning > 1:
|
|
118
|
+
cube = _bin_cube(cube, binning)
|
|
119
|
+
self._total_photons = int(cube.sum())
|
|
120
|
+
return np.ascontiguousarray(cube).astype(np.uint32)
|
|
121
|
+
|
|
122
|
+
def pixel_stack(self, channel=None, binning=1):
|
|
123
|
+
return self.raw_pixel_stack(channel=channel, binning=binning).astype(float)
|
|
124
|
+
|
|
125
|
+
def intensity_image(self, channel=None, binning=1):
|
|
126
|
+
return self.raw_pixel_stack(channel=channel, binning=binning).sum(axis=2)
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def photons_per_pulse(self):
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
class _GohlkeSignalFile(SignalReader):
|
|
133
|
+
_reader_name = None
|
|
134
|
+
_label = None
|
|
135
|
+
_reader_kwargs = {}
|
|
136
|
+
|
|
137
|
+
def __init__(self, path, verbose=True, channel=None, **kwargs):
|
|
138
|
+
pio = _import_io()
|
|
139
|
+
fn = getattr(pio, self._reader_name, None)
|
|
140
|
+
if fn is None:
|
|
141
|
+
raise ImportError(f'{self._reader_name} is not available in this '
|
|
142
|
+
f'phasorpy version; upgrade phasorpy')
|
|
143
|
+
signal = fn(str(path), **self._reader_kwargs)
|
|
144
|
+
super().__init__(path, signal, label=self._label, verbose=verbose,
|
|
145
|
+
channel=channel, **kwargs)
|
|
146
|
+
|
|
147
|
+
class PQBinFile(_GohlkeSignalFile):
|
|
148
|
+
_reader_name = 'signal_from_pqbin'
|
|
149
|
+
_label = 'PicoQuant BIN'
|
|
150
|
+
|
|
151
|
+
class SimfcsBHFile(_GohlkeSignalFile):
|
|
152
|
+
_reader_name = 'signal_from_bh'
|
|
153
|
+
_label = 'SimFCS B&H'
|
|
154
|
+
|
|
155
|
+
class SimfcsBHZFile(_GohlkeSignalFile):
|
|
156
|
+
_reader_name = 'signal_from_bhz'
|
|
157
|
+
_label = 'SimFCS BHZ'
|
|
158
|
+
|
|
159
|
+
class ImspectorTIFFFile(_GohlkeSignalFile):
|
|
160
|
+
_reader_name = 'signal_from_imspector_tiff'
|
|
161
|
+
_label = 'ImSpector FLIM TIFF'
|
|
162
|
+
|
|
163
|
+
class VistaTdflimFile(_GohlkeSignalFile):
|
|
164
|
+
_reader_name = 'signal_from_tdflim'
|
|
165
|
+
_label = 'ISS Vista TDFLIM'
|
|
166
|
+
_reader_kwargs = {'channel': None}
|
|
167
|
+
|
|
168
|
+
class FlimLabsSignalFile(_GohlkeSignalFile):
|
|
169
|
+
_reader_name = 'signal_from_flimlabs_json'
|
|
170
|
+
_label = 'FLIM LABS imaging'
|