phasorpy 0.5__cp312-cp312-win_arm64.whl → 0.6__cp312-cp312-win_arm64.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.
- phasorpy/__init__.py +2 -3
- phasorpy/_phasorpy.cp312-win_arm64.pyd +0 -0
- phasorpy/_phasorpy.pyx +185 -2
- phasorpy/_utils.py +121 -9
- phasorpy/cli.py +56 -3
- phasorpy/cluster.py +42 -6
- phasorpy/components.py +226 -55
- phasorpy/experimental.py +312 -0
- phasorpy/io/__init__.py +137 -0
- phasorpy/io/_flimlabs.py +350 -0
- phasorpy/io/_leica.py +329 -0
- phasorpy/io/_ometiff.py +445 -0
- phasorpy/io/_other.py +782 -0
- phasorpy/io/_simfcs.py +627 -0
- phasorpy/phasor.py +307 -1
- phasorpy/plot/__init__.py +27 -0
- phasorpy/plot/_functions.py +717 -0
- phasorpy/plot/_lifetime_plots.py +553 -0
- phasorpy/plot/_phasorplot.py +1119 -0
- phasorpy/plot/_phasorplot_fret.py +559 -0
- phasorpy/utils.py +84 -296
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/METADATA +2 -2
- phasorpy-0.6.dist-info/RECORD +34 -0
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/WHEEL +1 -1
- phasorpy/_io.py +0 -2655
- phasorpy/io.py +0 -9
- phasorpy/plot.py +0 -2318
- phasorpy/version.py +0 -80
- phasorpy-0.5.dist-info/RECORD +0 -26
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/licenses/LICENSE.txt +0 -0
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/top_level.txt +0 -0
phasorpy/io/_flimlabs.py
ADDED
@@ -0,0 +1,350 @@
|
|
1
|
+
"""Read FLIM LABS file formats."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
__all__ = ['phasor_from_flimlabs_json', 'signal_from_flimlabs_json']
|
6
|
+
|
7
|
+
import json
|
8
|
+
from typing import TYPE_CHECKING
|
9
|
+
|
10
|
+
from .._utils import parse_harmonic, xarray_metadata
|
11
|
+
|
12
|
+
if TYPE_CHECKING:
|
13
|
+
from .._typing import (
|
14
|
+
Any,
|
15
|
+
DataArray,
|
16
|
+
DTypeLike,
|
17
|
+
Literal,
|
18
|
+
NDArray,
|
19
|
+
PathLike,
|
20
|
+
Sequence,
|
21
|
+
)
|
22
|
+
|
23
|
+
import numpy
|
24
|
+
|
25
|
+
|
26
|
+
def phasor_from_flimlabs_json(
|
27
|
+
filename: str | PathLike[Any],
|
28
|
+
/,
|
29
|
+
channel: int | None = 0,
|
30
|
+
harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
|
31
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], dict[str, Any]]:
|
32
|
+
"""Return phasor coordinates and metadata from FLIM LABS JSON phasor file.
|
33
|
+
|
34
|
+
FLIM LABS JSON files may contain calibrated phasor coordinates
|
35
|
+
(possibly for multiple channels and harmonics) and metadata from
|
36
|
+
digital frequency-domain measurements.
|
37
|
+
|
38
|
+
Parameters
|
39
|
+
----------
|
40
|
+
filename : str or Path
|
41
|
+
Name of FLIM LABS JSON phasor file to read.
|
42
|
+
The file name usually contains the string "_phasor".
|
43
|
+
channel : int, optional
|
44
|
+
Index of channel to return.
|
45
|
+
By default, return the first channel.
|
46
|
+
If None, return all channels.
|
47
|
+
harmonic : int, sequence of int, or 'all', optional
|
48
|
+
Harmonic(s) to return from file.
|
49
|
+
If None (default), return the first harmonic stored in the file.
|
50
|
+
If `'all'`, return all harmonics as stored in file.
|
51
|
+
If a list, the first axes of the returned `real` and `imag` arrays
|
52
|
+
contain specified harmonic(s).
|
53
|
+
If an integer, the returned `real` and `imag` arrays are single
|
54
|
+
harmonic and have the same shape as `mean`.
|
55
|
+
|
56
|
+
Returns
|
57
|
+
-------
|
58
|
+
mean : ndarray
|
59
|
+
Average intensity image.
|
60
|
+
Zeroed if an intensity image is not present in file.
|
61
|
+
real : ndarray
|
62
|
+
Image of real component of phasor coordinates.
|
63
|
+
imag : ndarray
|
64
|
+
Image of imaginary component of phasor coordinates.
|
65
|
+
attrs : dict
|
66
|
+
Select metadata:
|
67
|
+
|
68
|
+
- ``'dims'`` (tuple of str):
|
69
|
+
:ref:`Axes codes <axes>` for `mean` image dimensions.
|
70
|
+
- ``'harmonic'`` (int):
|
71
|
+
Harmonic of `real` and `imag`.
|
72
|
+
- ``'frequency'`` (float):
|
73
|
+
Fundamental frequency of time-resolved phasor coordinates in MHz.
|
74
|
+
- ``'flimlabs_header'`` (dict):
|
75
|
+
FLIM LABS file header.
|
76
|
+
|
77
|
+
Raises
|
78
|
+
------
|
79
|
+
ValueError
|
80
|
+
File is not a FLIM LABS JSON file containing phasor coordinates.
|
81
|
+
IndexError
|
82
|
+
Harmonic or channel not found in file.
|
83
|
+
|
84
|
+
See Also
|
85
|
+
--------
|
86
|
+
phasorpy.io.signal_from_flimlabs_json
|
87
|
+
|
88
|
+
Examples
|
89
|
+
--------
|
90
|
+
>>> mean, real, imag, attrs = phasor_from_flimlabs_json(
|
91
|
+
... fetch('Convallaria_m2_1740751781_phasor_ch1.json'), harmonic='all'
|
92
|
+
... )
|
93
|
+
>>> real.shape
|
94
|
+
(3, 256, 256)
|
95
|
+
>>> attrs['dims']
|
96
|
+
('Y', 'X')
|
97
|
+
>>> attrs['harmonic']
|
98
|
+
[1, 2, 3]
|
99
|
+
>>> attrs['frequency'] # doctest: +NUMBER
|
100
|
+
40.00
|
101
|
+
|
102
|
+
"""
|
103
|
+
with open(filename, 'rb') as fh:
|
104
|
+
try:
|
105
|
+
data = json.load(fh)
|
106
|
+
except Exception as exc:
|
107
|
+
raise ValueError('not a valid JSON file') from exc
|
108
|
+
|
109
|
+
if (
|
110
|
+
'header' not in data
|
111
|
+
or 'phasors_data' not in data
|
112
|
+
or 'laser_period_ns' not in data['header']
|
113
|
+
or 'file_id' not in data['header']
|
114
|
+
# or data['header']['file_id'] != [73, 80, 71, 49] # 'IPG1'
|
115
|
+
):
|
116
|
+
raise ValueError(
|
117
|
+
'not a FLIM LABS JSON file containing phasor coordinates'
|
118
|
+
)
|
119
|
+
|
120
|
+
header = data['header']
|
121
|
+
phasor_data = data['phasors_data']
|
122
|
+
|
123
|
+
harmonics = []
|
124
|
+
channels = [] # 1-based
|
125
|
+
for d in phasor_data:
|
126
|
+
h = d['harmonic']
|
127
|
+
if h not in harmonics:
|
128
|
+
harmonics.append(h)
|
129
|
+
c = d['channel']
|
130
|
+
if c not in channels:
|
131
|
+
channels.append(c)
|
132
|
+
harmonics = sorted(harmonics)
|
133
|
+
channels = sorted(channels)
|
134
|
+
|
135
|
+
if channel is not None:
|
136
|
+
if channel + 1 not in channels:
|
137
|
+
raise IndexError(f'{channel=}')
|
138
|
+
channel += 1 # 1-based index
|
139
|
+
|
140
|
+
if isinstance(harmonic, str) and harmonic == 'all':
|
141
|
+
harmonic = harmonics
|
142
|
+
keep_harmonic_axis = True
|
143
|
+
else:
|
144
|
+
harmonic, keep_harmonic_axis = parse_harmonic(harmonic, harmonics[-1])
|
145
|
+
if any(h not in harmonics for h in harmonic):
|
146
|
+
raise IndexError(f'{harmonic=} not in {harmonics!r}')
|
147
|
+
harmonic_index = {h: i for i, h in enumerate(harmonic)}
|
148
|
+
|
149
|
+
nharmonics = len(harmonic)
|
150
|
+
nchannels = len(channels) if channel is None else 1
|
151
|
+
height = header['image_height']
|
152
|
+
width = header['image_width']
|
153
|
+
dtype = numpy.float32
|
154
|
+
|
155
|
+
shape: tuple[int, ...] = nharmonics, nchannels, height, width
|
156
|
+
axes: str = 'CYX'
|
157
|
+
mean = numpy.zeros(shape[1:], dtype)
|
158
|
+
real = numpy.zeros(shape, dtype)
|
159
|
+
imag = numpy.zeros(shape, dtype)
|
160
|
+
|
161
|
+
for d in phasor_data:
|
162
|
+
h = d['harmonic']
|
163
|
+
if h not in harmonic_index:
|
164
|
+
continue
|
165
|
+
h = harmonic_index[h]
|
166
|
+
if channel is not None:
|
167
|
+
if d['channel'] != channel:
|
168
|
+
continue
|
169
|
+
c = 0
|
170
|
+
else:
|
171
|
+
c = channels.index(d['channel'])
|
172
|
+
|
173
|
+
real[h, c] = numpy.asarray(d['g_data'], dtype)
|
174
|
+
imag[h, c] = numpy.asarray(d['s_data'], dtype)
|
175
|
+
|
176
|
+
if 'intensities_data' in data:
|
177
|
+
from .._phasorpy import _flimlabs_mean
|
178
|
+
|
179
|
+
mean.shape = nchannels, height * width
|
180
|
+
_flimlabs_mean(
|
181
|
+
mean,
|
182
|
+
data['intensities_data'],
|
183
|
+
-1 if channel is None else channels.index(channel),
|
184
|
+
)
|
185
|
+
mean.shape = shape[1:]
|
186
|
+
# JSON cannot store NaN values
|
187
|
+
nan_mask = mean == 0
|
188
|
+
real[:, nan_mask] = numpy.nan
|
189
|
+
imag[:, nan_mask] = numpy.nan
|
190
|
+
del nan_mask
|
191
|
+
|
192
|
+
if nchannels == 1:
|
193
|
+
axes = axes[1:]
|
194
|
+
mean = mean[0]
|
195
|
+
real = real[:, 0]
|
196
|
+
imag = imag[:, 0]
|
197
|
+
|
198
|
+
if not keep_harmonic_axis:
|
199
|
+
real = real[0]
|
200
|
+
imag = imag[0]
|
201
|
+
|
202
|
+
attrs = {
|
203
|
+
'dims': tuple(axes),
|
204
|
+
'samples': 256,
|
205
|
+
'harmonic': harmonic if keep_harmonic_axis else harmonic[0],
|
206
|
+
'frequency': 1000.0 / header['laser_period_ns'],
|
207
|
+
'flimlabs_header': header,
|
208
|
+
}
|
209
|
+
|
210
|
+
return mean, real, imag, attrs
|
211
|
+
|
212
|
+
|
213
|
+
def signal_from_flimlabs_json(
|
214
|
+
filename: str | PathLike[Any],
|
215
|
+
/,
|
216
|
+
*,
|
217
|
+
channel: int | None = 0,
|
218
|
+
dtype: DTypeLike | None = None,
|
219
|
+
) -> DataArray:
|
220
|
+
"""Return TCSPC histogram and metadata from FLIM LABS JSON imaging file.
|
221
|
+
|
222
|
+
FLIM LABS JSON imaging files contain encoded, multi-channel TCSPC
|
223
|
+
histograms and metadata from digital frequency-domain measurements.
|
224
|
+
|
225
|
+
Parameters
|
226
|
+
----------
|
227
|
+
filename : str or Path
|
228
|
+
Name of FLIM LABS JSON imaging file to read.
|
229
|
+
The file name usually contains the string "_imaging" or "_phasor".
|
230
|
+
channel : int, optional
|
231
|
+
Index of channel to return.
|
232
|
+
By default, return the first channel.
|
233
|
+
If None, return all channels.
|
234
|
+
dtype : dtype-like, optional, default: uint16
|
235
|
+
Unsigned integer type of TCSPC histogram.
|
236
|
+
Increase the bit-depth for high photon counts.
|
237
|
+
|
238
|
+
Returns
|
239
|
+
-------
|
240
|
+
xarray.DataArray
|
241
|
+
TCSPC histogram with :ref:`axes codes <axes>` ``'CYXH'`` and
|
242
|
+
type specified in ``dtype``:
|
243
|
+
|
244
|
+
- ``coords['H']``: delay-times of histogram bins in ns.
|
245
|
+
- ``attrs['frequency']``: laser repetition frequency in MHz.
|
246
|
+
- ``attrs['flimlabs_header']``: FLIM LABS file header.
|
247
|
+
|
248
|
+
Raises
|
249
|
+
------
|
250
|
+
ValueError
|
251
|
+
File is not a FLIM LABS JSON file containing TCSPC histogram.
|
252
|
+
`dtype` is not an unsigned integer.
|
253
|
+
IndexError
|
254
|
+
Channel out of range.
|
255
|
+
|
256
|
+
See Also
|
257
|
+
--------
|
258
|
+
phasorpy.io.phasor_from_flimlabs_json
|
259
|
+
|
260
|
+
Examples
|
261
|
+
--------
|
262
|
+
>>> signal = signal_from_flimlabs_json(
|
263
|
+
... fetch('Convallaria_m2_1740751781_phasor_ch1.json')
|
264
|
+
... )
|
265
|
+
>>> signal.values
|
266
|
+
array(...)
|
267
|
+
>>> signal.shape
|
268
|
+
(256, 256, 256)
|
269
|
+
>>> signal.dims
|
270
|
+
('Y', 'X', 'H')
|
271
|
+
>>> signal.coords['H'].data
|
272
|
+
array(...)
|
273
|
+
>>> signal.attrs['frequency'] # doctest: +NUMBER
|
274
|
+
40.00
|
275
|
+
|
276
|
+
"""
|
277
|
+
with open(filename, 'rb') as fh:
|
278
|
+
try:
|
279
|
+
data = json.load(fh)
|
280
|
+
except Exception as exc:
|
281
|
+
raise ValueError('not a valid JSON file') from exc
|
282
|
+
|
283
|
+
if (
|
284
|
+
'header' not in data
|
285
|
+
or 'laser_period_ns' not in data['header']
|
286
|
+
or 'file_id' not in data['header']
|
287
|
+
or ('data' not in data and 'intensities_data' not in data)
|
288
|
+
):
|
289
|
+
raise ValueError(
|
290
|
+
'not a FLIM LABS JSON file containing TCSPC histogram'
|
291
|
+
)
|
292
|
+
|
293
|
+
if dtype is None:
|
294
|
+
dtype = numpy.uint16
|
295
|
+
else:
|
296
|
+
dtype = numpy.dtype(dtype)
|
297
|
+
if dtype.kind != 'u':
|
298
|
+
raise ValueError(f'{dtype=} is not an unsigned integer type')
|
299
|
+
|
300
|
+
header = data['header']
|
301
|
+
nchannels = len([c for c in header['channels'] if c])
|
302
|
+
height = header['image_height']
|
303
|
+
width = header['image_width']
|
304
|
+
frequency = 1000.0 / header['laser_period_ns']
|
305
|
+
|
306
|
+
if channel is not None:
|
307
|
+
if channel >= nchannels or channel < 0:
|
308
|
+
raise IndexError(f'{channel=} out of range[0, {nchannels=}]')
|
309
|
+
nchannels = 1
|
310
|
+
|
311
|
+
if 'data' in data:
|
312
|
+
# file_id = [73, 77, 71, 49] # 'IMG1'
|
313
|
+
intensities_data = data['data']
|
314
|
+
else:
|
315
|
+
# file_id = [73, 80, 71, 49] # 'IPG1'
|
316
|
+
intensities_data = data['intensities_data']
|
317
|
+
|
318
|
+
from .._phasorpy import _flimlabs_signal
|
319
|
+
|
320
|
+
signal = numpy.zeros((nchannels, height * width, 256), dtype)
|
321
|
+
_flimlabs_signal(
|
322
|
+
signal,
|
323
|
+
intensities_data,
|
324
|
+
-1 if channel is None else channel,
|
325
|
+
)
|
326
|
+
|
327
|
+
if channel is None and nchannels > 1:
|
328
|
+
signal.shape = (nchannels, height, width, 256)
|
329
|
+
axes = 'CYXH'
|
330
|
+
else:
|
331
|
+
signal.shape = (height, width, 256)
|
332
|
+
axes = 'YXH'
|
333
|
+
|
334
|
+
coords: dict[str, Any] = {}
|
335
|
+
coords['H'] = numpy.linspace(
|
336
|
+
0.0, header['laser_period_ns'], 256, endpoint=False
|
337
|
+
)
|
338
|
+
if channel is None and nchannels > 1:
|
339
|
+
coords['C'] = numpy.asarray(
|
340
|
+
[i for i, c in enumerate(header['channels']) if c]
|
341
|
+
)
|
342
|
+
|
343
|
+
metadata = xarray_metadata(axes, signal.shape, filename, **coords)
|
344
|
+
attrs = metadata['attrs']
|
345
|
+
attrs['frequency'] = frequency
|
346
|
+
attrs['flimlabs_header'] = header
|
347
|
+
|
348
|
+
from xarray import DataArray
|
349
|
+
|
350
|
+
return DataArray(signal, **metadata)
|
phasorpy/io/_leica.py
ADDED
@@ -0,0 +1,329 @@
|
|
1
|
+
"""Read Leica image file formats."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
__all__ = ['lifetime_from_lif', 'phasor_from_lif', 'signal_from_lif']
|
6
|
+
|
7
|
+
from typing import TYPE_CHECKING
|
8
|
+
|
9
|
+
from .._utils import xarray_metadata
|
10
|
+
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from .._typing import Any, DataArray, Literal, NDArray, PathLike
|
13
|
+
|
14
|
+
import numpy
|
15
|
+
|
16
|
+
|
17
|
+
def phasor_from_lif(
|
18
|
+
filename: str | PathLike[Any],
|
19
|
+
/,
|
20
|
+
image: str | None = None,
|
21
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], dict[str, Any]]:
|
22
|
+
"""Return phasor coordinates and metadata from Leica image file.
|
23
|
+
|
24
|
+
Leica image files may contain uncalibrated phasor coordinate images and
|
25
|
+
metadata from the analysis of FLIM measurements.
|
26
|
+
|
27
|
+
Parameters
|
28
|
+
----------
|
29
|
+
filename : str or Path
|
30
|
+
Name of Leica image file to read.
|
31
|
+
image : str, optional
|
32
|
+
Name of parent image containing phasor coordinates.
|
33
|
+
|
34
|
+
Returns
|
35
|
+
-------
|
36
|
+
mean : ndarray
|
37
|
+
Average intensity image.
|
38
|
+
real : ndarray
|
39
|
+
Image of real component of phasor coordinates.
|
40
|
+
imag : ndarray
|
41
|
+
Image of imaginary component of phasor coordinates.
|
42
|
+
attrs : dict
|
43
|
+
Select metadata:
|
44
|
+
|
45
|
+
- ``'dims'`` (tuple of str):
|
46
|
+
:ref:`Axes codes <axes>` for `mean` image dimensions.
|
47
|
+
- ``'frequency'`` (float):
|
48
|
+
Fundamental frequency of time-resolved phasor coordinates in MHz.
|
49
|
+
May not be present in all files.
|
50
|
+
- ``'flim_rawdata'`` (dict):
|
51
|
+
Settings from SingleMoleculeDetection/RawData XML element.
|
52
|
+
- ``'flim_phasor_channels'`` (list of dict):
|
53
|
+
Settings from SingleMoleculeDetection/.../PhasorData/Channels XML
|
54
|
+
elements.
|
55
|
+
|
56
|
+
Raises
|
57
|
+
------
|
58
|
+
liffile.LifFileError
|
59
|
+
File is not a Leica image file.
|
60
|
+
ValueError
|
61
|
+
File or `image` do not contain phasor coordinates and metadata.
|
62
|
+
|
63
|
+
Notes
|
64
|
+
-----
|
65
|
+
The implementation is based on the
|
66
|
+
`liffile <https://github.com/cgohlke/liffile/>`__ library.
|
67
|
+
|
68
|
+
Examples
|
69
|
+
--------
|
70
|
+
>>> mean, real, imag, attrs = phasor_from_lif(fetch('FLIM_testdata.lif'))
|
71
|
+
>>> real.shape
|
72
|
+
(1024, 1024)
|
73
|
+
>>> attrs['dims']
|
74
|
+
('Y', 'X')
|
75
|
+
>>> attrs['frequency']
|
76
|
+
19.505
|
77
|
+
|
78
|
+
"""
|
79
|
+
# TODO: read harmonic from XML if possible
|
80
|
+
# TODO: get calibration settings from XML metadata, lifetime, or
|
81
|
+
# phasor plot images
|
82
|
+
import liffile
|
83
|
+
|
84
|
+
image = '' if image is None else f'.*{image}.*/'
|
85
|
+
samples = 1
|
86
|
+
|
87
|
+
with liffile.LifFile(filename) as lif:
|
88
|
+
try:
|
89
|
+
im = lif.images[image + 'Phasor Intensity$']
|
90
|
+
dims = im.dims
|
91
|
+
coords = im.coords
|
92
|
+
# meta = image.attrs
|
93
|
+
mean = im.asarray().astype(numpy.float32)
|
94
|
+
real = lif.images[image + 'Phasor Real$'].asarray()
|
95
|
+
imag = lif.images[image + 'Phasor Imaginary$'].asarray()
|
96
|
+
# mask = lif.images[image + 'Phasor Mask$'].asarray()
|
97
|
+
except Exception as exc:
|
98
|
+
raise ValueError(
|
99
|
+
f'{lif.filename!r} does not contain Phasor images'
|
100
|
+
) from exc
|
101
|
+
|
102
|
+
attrs: dict[str, Any] = {'dims': dims, 'coords': coords}
|
103
|
+
flim = im.parent_image
|
104
|
+
if flim is not None and isinstance(flim, liffile.LifFlimImage):
|
105
|
+
xml = flim.parent.xml_element
|
106
|
+
frequency = xml.find('.//Dataset/RawData/LaserPulseFrequency')
|
107
|
+
if frequency is not None and frequency.text is not None:
|
108
|
+
attrs['frequency'] = float(frequency.text) * 1e-6
|
109
|
+
clock_period = xml.find('.//Dataset/RawData/ClockPeriod')
|
110
|
+
if clock_period is not None and clock_period.text is not None:
|
111
|
+
tmp = float(clock_period.text) * float(frequency.text)
|
112
|
+
samples = int(round(1.0 / tmp))
|
113
|
+
attrs['samples'] = samples
|
114
|
+
channels = []
|
115
|
+
for channel in xml.findall(
|
116
|
+
'.//Dataset/FlimData/PhasorData/Channels'
|
117
|
+
):
|
118
|
+
ch = liffile.xml2dict(channel)['Channels']
|
119
|
+
ch.pop('PhasorPlotShapes', None)
|
120
|
+
channels.append(ch)
|
121
|
+
attrs['flim_phasor_channels'] = channels
|
122
|
+
attrs['flim_rawdata'] = flim.attrs.get('RawData', {})
|
123
|
+
|
124
|
+
if samples > 1:
|
125
|
+
mean /= samples
|
126
|
+
return (
|
127
|
+
mean,
|
128
|
+
real.astype(numpy.float32),
|
129
|
+
imag.astype(numpy.float32),
|
130
|
+
attrs,
|
131
|
+
)
|
132
|
+
|
133
|
+
|
134
|
+
def lifetime_from_lif(
|
135
|
+
filename: str | PathLike[Any],
|
136
|
+
/,
|
137
|
+
image: str | None = None,
|
138
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], dict[str, Any]]:
|
139
|
+
"""Return lifetime image and metadata from Leica image file.
|
140
|
+
|
141
|
+
Leica image files may contain fluorescence lifetime images and metadata
|
142
|
+
from the analysis of FLIM measurements.
|
143
|
+
|
144
|
+
Parameters
|
145
|
+
----------
|
146
|
+
filename : str or Path
|
147
|
+
Name of Leica image file to read.
|
148
|
+
image : str, optional
|
149
|
+
Name of parent image containing lifetime image.
|
150
|
+
|
151
|
+
Returns
|
152
|
+
-------
|
153
|
+
lifetime : ndarray
|
154
|
+
Fluorescence lifetime image in ns.
|
155
|
+
intensity : ndarray
|
156
|
+
Fluorescence intensity image.
|
157
|
+
stddev : ndarray
|
158
|
+
Standard deviation of fluorescence lifetimes in ns.
|
159
|
+
attrs : dict
|
160
|
+
Select metadata:
|
161
|
+
|
162
|
+
- ``'dims'`` (tuple of str):
|
163
|
+
:ref:`Axes codes <axes>` for `intensity` image dimensions.
|
164
|
+
- ``'frequency'`` (float):
|
165
|
+
Fundamental frequency of lifetimes in MHz.
|
166
|
+
May not be present in all files.
|
167
|
+
- ``'samples'`` (int):
|
168
|
+
Number of bins in TCSPC histogram. May not be present in all files.
|
169
|
+
- ``'flim_rawdata'`` (dict):
|
170
|
+
Settings from SingleMoleculeDetection/RawData XML element.
|
171
|
+
|
172
|
+
Raises
|
173
|
+
------
|
174
|
+
liffile.LifFileError
|
175
|
+
File is not a Leica image file.
|
176
|
+
ValueError
|
177
|
+
File or `image` does not contain lifetime coordinates and metadata.
|
178
|
+
|
179
|
+
Notes
|
180
|
+
-----
|
181
|
+
The implementation is based on the
|
182
|
+
`liffile <https://github.com/cgohlke/liffile/>`__ library.
|
183
|
+
|
184
|
+
Examples
|
185
|
+
--------
|
186
|
+
>>> lifetime, intensity, stddev, attrs = lifetime_from_lif(
|
187
|
+
... fetch('FLIM_testdata.lif')
|
188
|
+
... )
|
189
|
+
>>> lifetime.shape
|
190
|
+
(1024, 1024)
|
191
|
+
>>> attrs['dims']
|
192
|
+
('Y', 'X')
|
193
|
+
>>> attrs['frequency']
|
194
|
+
19.505
|
195
|
+
|
196
|
+
"""
|
197
|
+
import liffile
|
198
|
+
|
199
|
+
image = '' if image is None else f'.*{image}.*/'
|
200
|
+
|
201
|
+
with liffile.LifFile(filename) as lif:
|
202
|
+
try:
|
203
|
+
im = lif.images[image + 'Intensity$']
|
204
|
+
dims = im.dims
|
205
|
+
coords = im.coords
|
206
|
+
# meta = im.attrs
|
207
|
+
intensity = im.asarray()
|
208
|
+
lifetime = lif.images[image + 'Fast Flim$'].asarray()
|
209
|
+
stddev = lif.images[image + 'Standard Deviation$'].asarray()
|
210
|
+
except Exception as exc:
|
211
|
+
raise ValueError(
|
212
|
+
f'{lif.filename!r} does not contain lifetime images'
|
213
|
+
) from exc
|
214
|
+
|
215
|
+
attrs: dict[str, Any] = {'dims': dims, 'coords': coords}
|
216
|
+
flim = im.parent_image
|
217
|
+
if flim is not None and isinstance(flim, liffile.LifFlimImage):
|
218
|
+
xml = flim.parent.xml_element
|
219
|
+
frequency = xml.find('.//Dataset/RawData/LaserPulseFrequency')
|
220
|
+
if frequency is not None and frequency.text is not None:
|
221
|
+
attrs['frequency'] = float(frequency.text) * 1e-6
|
222
|
+
clock_period = xml.find('.//Dataset/RawData/ClockPeriod')
|
223
|
+
if clock_period is not None and clock_period.text is not None:
|
224
|
+
tmp = float(clock_period.text) * float(frequency.text)
|
225
|
+
samples = int(round(1.0 / tmp))
|
226
|
+
attrs['samples'] = samples
|
227
|
+
attrs['flim_rawdata'] = flim.attrs.get('RawData', {})
|
228
|
+
|
229
|
+
return (
|
230
|
+
lifetime.astype(numpy.float32),
|
231
|
+
intensity.astype(numpy.float32),
|
232
|
+
stddev.astype(numpy.float32),
|
233
|
+
attrs,
|
234
|
+
)
|
235
|
+
|
236
|
+
|
237
|
+
def signal_from_lif(
|
238
|
+
filename: str | PathLike[Any],
|
239
|
+
/,
|
240
|
+
*,
|
241
|
+
image: int | str | None = None,
|
242
|
+
dim: Literal['λ', 'Λ'] | str = 'λ',
|
243
|
+
) -> DataArray:
|
244
|
+
"""Return hyperspectral image and metadata from Leica image file.
|
245
|
+
|
246
|
+
Leica image files may contain hyperspectral images and metadata from laser
|
247
|
+
scanning microscopy measurements.
|
248
|
+
|
249
|
+
Parameters
|
250
|
+
----------
|
251
|
+
filename : str or Path
|
252
|
+
Name of Leica image file to read.
|
253
|
+
image : str or int, optional
|
254
|
+
Index or regex pattern of image to return.
|
255
|
+
By default, return the first image containing hyperspectral data.
|
256
|
+
dim : str or None
|
257
|
+
Character code of hyperspectral dimension.
|
258
|
+
Either ``'λ'`` for emission (default) or ``'Λ'`` for excitation.
|
259
|
+
|
260
|
+
Returns
|
261
|
+
-------
|
262
|
+
xarray.DataArray
|
263
|
+
Hyperspectral image data.
|
264
|
+
|
265
|
+
- ``coords['C']``: wavelengths in nm.
|
266
|
+
- ``coords['T']``: time coordinates in s, if any.
|
267
|
+
|
268
|
+
Raises
|
269
|
+
------
|
270
|
+
liffile.LifFileError
|
271
|
+
File is not a Leica image file.
|
272
|
+
ValueError
|
273
|
+
File is not a Leica image file or does not contain hyperspectral image.
|
274
|
+
|
275
|
+
Notes
|
276
|
+
-----
|
277
|
+
The implementation is based on the
|
278
|
+
`liffile <https://github.com/cgohlke/liffile/>`__ library.
|
279
|
+
|
280
|
+
Reading of TCSPC histograms from FLIM measurements is not supported
|
281
|
+
because the compression scheme is patent-pending.
|
282
|
+
|
283
|
+
Examples
|
284
|
+
--------
|
285
|
+
>>> signal = signal_from_lif('ScanModesExamples.lif') # doctest: +SKIP
|
286
|
+
>>> signal.values # doctest: +SKIP
|
287
|
+
array(...)
|
288
|
+
>>> signal.shape # doctest: +SKIP
|
289
|
+
(9, 128, 128)
|
290
|
+
>>> signal.dims # doctest: +SKIP
|
291
|
+
('C', 'Y', 'X')
|
292
|
+
>>> signal.coords['C'].data # doctest: +SKIP
|
293
|
+
array([560, 580, 600, ..., 680, 700, 720])
|
294
|
+
|
295
|
+
"""
|
296
|
+
import liffile
|
297
|
+
|
298
|
+
with liffile.LifFile(filename) as lif:
|
299
|
+
if image is None:
|
300
|
+
# find image with excitation or emission dimension
|
301
|
+
for im in lif.images:
|
302
|
+
if dim in im.dims:
|
303
|
+
break
|
304
|
+
else:
|
305
|
+
raise ValueError(
|
306
|
+
f'{lif.filename!r} does not contain hyperspectral image'
|
307
|
+
)
|
308
|
+
else:
|
309
|
+
im = lif.images[image]
|
310
|
+
|
311
|
+
if dim not in im.dims or im.sizes[dim] < 4:
|
312
|
+
raise ValueError(f'{im!r} does not contain spectral dimension')
|
313
|
+
if 'C' in im.dims:
|
314
|
+
raise ValueError(
|
315
|
+
'hyperspectral image must not contain channel axis'
|
316
|
+
)
|
317
|
+
|
318
|
+
data = im.asarray()
|
319
|
+
coords: dict[str, Any] = {
|
320
|
+
('C' if k == dim else k): (v * 1e9 if k == dim else v)
|
321
|
+
for (k, v) in im.coords.items()
|
322
|
+
}
|
323
|
+
dims = tuple(('C' if d == dim else d) for d in im.dims)
|
324
|
+
|
325
|
+
metadata = xarray_metadata(dims, im.shape, filename, **coords)
|
326
|
+
|
327
|
+
from xarray import DataArray
|
328
|
+
|
329
|
+
return DataArray(data, **metadata)
|