phasorpy 0.5__cp312-cp312-win_amd64.whl → 0.7__cp312-cp312-win_amd64.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_amd64.pyd +0 -0
- phasorpy/_phasorpy.pyx +466 -11
- phasorpy/_utils.py +222 -37
- phasorpy/cli.py +74 -3
- phasorpy/cluster.py +51 -21
- phasorpy/color.py +11 -7
- phasorpy/component.py +707 -0
- phasorpy/{cursors.py → cursor.py} +31 -33
- phasorpy/datasets.py +117 -7
- phasorpy/experimental.py +310 -0
- phasorpy/io/__init__.py +138 -0
- phasorpy/io/_flimlabs.py +360 -0
- phasorpy/io/_leica.py +331 -0
- phasorpy/io/_ometiff.py +444 -0
- phasorpy/io/_other.py +890 -0
- phasorpy/io/_simfcs.py +652 -0
- phasorpy/lifetime.py +2058 -0
- phasorpy/phasor.py +184 -1754
- phasorpy/plot/__init__.py +27 -0
- phasorpy/plot/_functions.py +723 -0
- phasorpy/plot/_lifetime_plots.py +563 -0
- phasorpy/plot/_phasorplot.py +1507 -0
- phasorpy/plot/_phasorplot_fret.py +561 -0
- phasorpy/utils.py +89 -290
- {phasorpy-0.5.dist-info → phasorpy-0.7.dist-info}/METADATA +3 -3
- phasorpy-0.7.dist-info/RECORD +35 -0
- {phasorpy-0.5.dist-info → phasorpy-0.7.dist-info}/WHEEL +1 -1
- phasorpy/_io.py +0 -2655
- phasorpy/components.py +0 -313
- 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.7.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.5.dist-info → phasorpy-0.7.dist-info}/licenses/LICENSE.txt +0 -0
- {phasorpy-0.5.dist-info → phasorpy-0.7.dist-info}/top_level.txt +0 -0
phasorpy/io/_other.py
ADDED
@@ -0,0 +1,890 @@
|
|
1
|
+
"""Read other file formats."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
__all__ = [
|
6
|
+
# 'signal_from_czi',
|
7
|
+
'signal_from_flif',
|
8
|
+
'phasor_from_ifli',
|
9
|
+
'signal_from_imspector_tiff',
|
10
|
+
'signal_from_lsm',
|
11
|
+
'signal_from_pqbin',
|
12
|
+
'signal_from_ptu',
|
13
|
+
'signal_from_sdt',
|
14
|
+
]
|
15
|
+
|
16
|
+
import os
|
17
|
+
import struct
|
18
|
+
from typing import TYPE_CHECKING
|
19
|
+
from xml.etree import ElementTree
|
20
|
+
|
21
|
+
from .._utils import parse_harmonic, squeeze_dims, xarray_metadata
|
22
|
+
|
23
|
+
if TYPE_CHECKING:
|
24
|
+
from .._typing import (
|
25
|
+
Any,
|
26
|
+
DataArray,
|
27
|
+
DTypeLike,
|
28
|
+
Literal,
|
29
|
+
NDArray,
|
30
|
+
PathLike,
|
31
|
+
Sequence,
|
32
|
+
EllipsisType,
|
33
|
+
)
|
34
|
+
|
35
|
+
import numpy
|
36
|
+
|
37
|
+
|
38
|
+
def signal_from_sdt(
|
39
|
+
filename: str | PathLike[Any],
|
40
|
+
/,
|
41
|
+
*,
|
42
|
+
index: int = 0,
|
43
|
+
) -> DataArray:
|
44
|
+
"""Return TCSPC histogram and metadata from Becker & Hickl SDT file.
|
45
|
+
|
46
|
+
SDT files contain TCSPC measurement data and instrumentation parameters.
|
47
|
+
|
48
|
+
Parameters
|
49
|
+
----------
|
50
|
+
filename : str or Path
|
51
|
+
Name of Becker & Hickl SDT file to read.
|
52
|
+
index : int, optional, default: 0
|
53
|
+
Index of dataset to read if the file contains multiple datasets.
|
54
|
+
|
55
|
+
Returns
|
56
|
+
-------
|
57
|
+
xarray.DataArray
|
58
|
+
TCSPC histogram with :ref:`axes codes <axes>` ``'QCYXH'`` and
|
59
|
+
type ``uint16``, ``uint32``, or ``float32``.
|
60
|
+
Dimensions ``'Q'`` and ``'C'`` are optional detector channels.
|
61
|
+
|
62
|
+
- ``coords['H']``: delay-times of histogram bins in ns.
|
63
|
+
- ``attrs['frequency']``: repetition frequency in MHz.
|
64
|
+
|
65
|
+
Raises
|
66
|
+
------
|
67
|
+
ValueError
|
68
|
+
File is not an SDT file containing TCSPC histogram.
|
69
|
+
|
70
|
+
Notes
|
71
|
+
-----
|
72
|
+
The implementation is based on the
|
73
|
+
`sdtfile <https://github.com/cgohlke/sdtfile/>`__ library.
|
74
|
+
|
75
|
+
Examples
|
76
|
+
--------
|
77
|
+
>>> signal = signal_from_sdt(fetch('tcspc.sdt'))
|
78
|
+
>>> signal.values
|
79
|
+
array(...)
|
80
|
+
>>> signal.dtype
|
81
|
+
dtype('uint16')
|
82
|
+
>>> signal.shape
|
83
|
+
(128, 128, 256)
|
84
|
+
>>> signal.dims
|
85
|
+
('Y', 'X', 'H')
|
86
|
+
>>> signal.coords['H'].data
|
87
|
+
array([0, ..., 12.45])
|
88
|
+
>>> signal.attrs['frequency'] # doctest: +NUMBER
|
89
|
+
79.99
|
90
|
+
|
91
|
+
"""
|
92
|
+
import sdtfile
|
93
|
+
|
94
|
+
with sdtfile.SdtFile(filename) as sdt:
|
95
|
+
if (
|
96
|
+
'SPC Setup & Data File' not in sdt.info.id
|
97
|
+
and 'SPC FCS Data File' not in sdt.info.id
|
98
|
+
):
|
99
|
+
# skip DLL data
|
100
|
+
raise ValueError(
|
101
|
+
f'{os.path.basename(filename)!r} '
|
102
|
+
'is not an SDT file containing TCSPC data'
|
103
|
+
)
|
104
|
+
# filter block types?
|
105
|
+
# sdtfile.BlockType(sdt.block_headers[index].block_type).contents
|
106
|
+
# == 'PAGE_BLOCK'
|
107
|
+
data = sdt.data[index]
|
108
|
+
times = sdt.times[index] * 1e9
|
109
|
+
|
110
|
+
# TODO: get spatial coordinates from scanner settings?
|
111
|
+
metadata = xarray_metadata(
|
112
|
+
'QCYXH'[-data.ndim :], data.shape, filename, H=times
|
113
|
+
)
|
114
|
+
metadata['attrs']['frequency'] = 1e3 / float(times[-1] + times[1])
|
115
|
+
|
116
|
+
from xarray import DataArray
|
117
|
+
|
118
|
+
return DataArray(data, **metadata)
|
119
|
+
|
120
|
+
|
121
|
+
def signal_from_ptu(
|
122
|
+
filename: str | PathLike[Any],
|
123
|
+
/,
|
124
|
+
selection: Sequence[int | slice | EllipsisType | None] | None = None,
|
125
|
+
*,
|
126
|
+
trimdims: Sequence[Literal['T', 'C', 'H']] | str | None = None,
|
127
|
+
dtype: DTypeLike | None = None,
|
128
|
+
frame: int | None = None,
|
129
|
+
channel: int | None = 0,
|
130
|
+
dtime: int | None = 0,
|
131
|
+
keepdims: bool = False,
|
132
|
+
**kwargs: Any,
|
133
|
+
) -> DataArray:
|
134
|
+
"""Return TCSPC histogram and metadata from PicoQuant PTU T3 mode file.
|
135
|
+
|
136
|
+
PTU files contain TCSPC measurement data and instrumentation parameters,
|
137
|
+
which are decoded to a multi-dimensional TCSPC histogram.
|
138
|
+
|
139
|
+
Parameters
|
140
|
+
----------
|
141
|
+
filename : str or Path
|
142
|
+
Name of PTU file to read.
|
143
|
+
selection : sequence of index types, optional
|
144
|
+
Indices for all dimensions of image mode files:
|
145
|
+
|
146
|
+
- ``None``: return all items along axis (default).
|
147
|
+
- ``Ellipsis`` (``...``): return all items along multiple axes.
|
148
|
+
- ``int``: return single item along axis.
|
149
|
+
- ``slice``: return chunk of axis.
|
150
|
+
``slice.step`` is a binning factor.
|
151
|
+
If ``slice.step=-1``, integrate all items along axis.
|
152
|
+
|
153
|
+
trimdims : str, optional, default: 'TCH'
|
154
|
+
Axes to trim.
|
155
|
+
dtype : dtype_like, optional, default: uint16
|
156
|
+
Unsigned integer type of TCSPC histogram.
|
157
|
+
Increase the bit depth to avoid overflows when integrating.
|
158
|
+
frame : int, optional
|
159
|
+
If < 0, integrate time axis, else return specified frame.
|
160
|
+
Overrides `selection` for axis ``T``.
|
161
|
+
channel : int, optional
|
162
|
+
Index of channel to return.
|
163
|
+
By default, return the first channel.
|
164
|
+
If < 0, integrate channel axis.
|
165
|
+
Overrides `selection` for axis ``C``.
|
166
|
+
dtime : int, optional, default: 0
|
167
|
+
Specifies number of bins in TCSPC histogram.
|
168
|
+
If 0 (default), return the number of bins in one period.
|
169
|
+
If < 0, integrate delay-time axis (image mode only).
|
170
|
+
If > 0, return up to specified bin.
|
171
|
+
Overrides `selection` for axis ``H``.
|
172
|
+
keepdims : bool, optional, default: False
|
173
|
+
If true, return reduced axes as size-one dimensions.
|
174
|
+
**kwargs
|
175
|
+
Additional arguments passed to :py:meth:`PtuFile.decode_image`
|
176
|
+
or :py:meth:`PtuFile.decode_histogram`.
|
177
|
+
|
178
|
+
Returns
|
179
|
+
-------
|
180
|
+
xarray.DataArray
|
181
|
+
TCSPC histogram with :ref:`axes codes <axes>` ``'TYXCH'`` and
|
182
|
+
type specified in ``dtype``:
|
183
|
+
|
184
|
+
- ``coords['H']``: delay-times of histogram bins in ns.
|
185
|
+
- ``attrs['frequency']``: repetition frequency in MHz.
|
186
|
+
- ``attrs['ptu_tags']``: metadata read from PTU file.
|
187
|
+
|
188
|
+
Size-one dimensions are prepended to point mode data to make them
|
189
|
+
broadcastable to image data.
|
190
|
+
|
191
|
+
Raises
|
192
|
+
------
|
193
|
+
ptufile.PqFileError
|
194
|
+
File is not a PicoQuant PTU file or is corrupted.
|
195
|
+
ValueError
|
196
|
+
File is not a PicoQuant PTU T3 mode file containing TCSPC data.
|
197
|
+
|
198
|
+
Notes
|
199
|
+
-----
|
200
|
+
The implementation is based on the
|
201
|
+
`ptufile <https://github.com/cgohlke/ptufile/>`__ library.
|
202
|
+
|
203
|
+
Examples
|
204
|
+
--------
|
205
|
+
>>> signal = signal_from_ptu(fetch('hazelnut_FLIM_single_image.ptu'))
|
206
|
+
>>> signal.values
|
207
|
+
array(...)
|
208
|
+
>>> signal.dtype
|
209
|
+
dtype('uint16')
|
210
|
+
>>> signal.shape
|
211
|
+
(5, 256, 256, 132)
|
212
|
+
>>> signal.dims
|
213
|
+
('T', 'Y', 'X', 'H')
|
214
|
+
>>> signal.coords['H'].data
|
215
|
+
array([0, ..., 12.7])
|
216
|
+
>>> signal.attrs['frequency'] # doctest: +NUMBER
|
217
|
+
78.02
|
218
|
+
|
219
|
+
"""
|
220
|
+
import ptufile
|
221
|
+
from xarray import DataArray
|
222
|
+
|
223
|
+
kwargs.pop('records', None)
|
224
|
+
|
225
|
+
with ptufile.PtuFile(filename, trimdims=trimdims) as ptu:
|
226
|
+
if not ptu.is_t3:
|
227
|
+
raise ValueError(f'{ptu.filename!r} is not a T3 mode PTU file')
|
228
|
+
if ptu.is_image:
|
229
|
+
data = ptu.decode_image(
|
230
|
+
selection,
|
231
|
+
dtype=dtype,
|
232
|
+
frame=frame,
|
233
|
+
channel=channel,
|
234
|
+
dtime=dtime,
|
235
|
+
keepdims=keepdims,
|
236
|
+
asxarray=True,
|
237
|
+
**kwargs,
|
238
|
+
)
|
239
|
+
assert isinstance(data, DataArray)
|
240
|
+
elif ptu.measurement_submode == 1:
|
241
|
+
# point mode IRF
|
242
|
+
if dtime == -1:
|
243
|
+
raise ValueError(f'{dtime=} not supported for point mode')
|
244
|
+
data = ptu.decode_histogram(
|
245
|
+
dtype=dtype, dtime=dtime, asxarray=True, **kwargs
|
246
|
+
)
|
247
|
+
assert isinstance(data, DataArray)
|
248
|
+
if channel is not None:
|
249
|
+
if keepdims:
|
250
|
+
data = data[channel : channel + 1]
|
251
|
+
else:
|
252
|
+
data = data[channel]
|
253
|
+
# prepend dimensions as needed to appear image-like
|
254
|
+
data = data.expand_dims(dim={'Y': 1, 'X': 1})
|
255
|
+
if keepdims:
|
256
|
+
data = data.expand_dims(dim={'T': 1})
|
257
|
+
else:
|
258
|
+
raise ValueError(
|
259
|
+
f'{ptu.filename!r} is not a point or image mode PTU file'
|
260
|
+
)
|
261
|
+
|
262
|
+
data.attrs['ptu_tags'] = ptu.tags
|
263
|
+
data.attrs['frequency'] = ptu.frequency * 1e-6 # MHz
|
264
|
+
data.coords['H'] = data.coords['H'] * 1e9
|
265
|
+
|
266
|
+
return data
|
267
|
+
|
268
|
+
|
269
|
+
def signal_from_lsm(
|
270
|
+
filename: str | PathLike[Any],
|
271
|
+
/,
|
272
|
+
) -> DataArray:
|
273
|
+
"""Return hyperspectral image and metadata from Zeiss LSM file.
|
274
|
+
|
275
|
+
Zeiss LSM files contain multi-dimensional images and metadata from laser
|
276
|
+
scanning microscopy measurements. The file format is based on TIFF.
|
277
|
+
|
278
|
+
Parameters
|
279
|
+
----------
|
280
|
+
filename : str or Path
|
281
|
+
Name of Zeiss LSM file to read.
|
282
|
+
|
283
|
+
Returns
|
284
|
+
-------
|
285
|
+
xarray.DataArray
|
286
|
+
Hyperspectral image data.
|
287
|
+
Usually, a 3-to-5-dimensional array of type ``uint8`` or ``uint16``.
|
288
|
+
|
289
|
+
- ``coords['C']``: wavelengths in nm.
|
290
|
+
- ``coords['T']``: time coordinates in s, if any.
|
291
|
+
|
292
|
+
Raises
|
293
|
+
------
|
294
|
+
tifffile.TiffFileError
|
295
|
+
File is not a TIFF file.
|
296
|
+
ValueError
|
297
|
+
File is not an LSM file or does not contain hyperspectral image.
|
298
|
+
|
299
|
+
Notes
|
300
|
+
-----
|
301
|
+
The implementation is based on the
|
302
|
+
`tifffile <https://github.com/cgohlke/tifffile/>`__ library.
|
303
|
+
|
304
|
+
Examples
|
305
|
+
--------
|
306
|
+
>>> signal = signal_from_lsm(fetch('paramecium.lsm'))
|
307
|
+
>>> signal.values
|
308
|
+
array(...)
|
309
|
+
>>> signal.dtype
|
310
|
+
dtype('uint8')
|
311
|
+
>>> signal.shape
|
312
|
+
(30, 512, 512)
|
313
|
+
>>> signal.dims
|
314
|
+
('C', 'Y', 'X')
|
315
|
+
>>> signal.coords['C'].data # wavelengths
|
316
|
+
array([423, ..., 713])
|
317
|
+
|
318
|
+
"""
|
319
|
+
import tifffile
|
320
|
+
|
321
|
+
with tifffile.TiffFile(filename) as tif:
|
322
|
+
if not tif.is_lsm:
|
323
|
+
raise ValueError(f'{tif.filename!r} is not an LSM file')
|
324
|
+
|
325
|
+
page = tif.pages.first
|
326
|
+
lsminfo = tif.lsm_metadata
|
327
|
+
channels = page.tags[258].count
|
328
|
+
|
329
|
+
if channels < 4 or lsminfo is None or lsminfo['SpectralScan'] != 1:
|
330
|
+
raise ValueError(
|
331
|
+
f'{tif.filename!r} does not contain hyperspectral image'
|
332
|
+
)
|
333
|
+
|
334
|
+
# TODO: contribute this to tifffile
|
335
|
+
series = tif.series[0]
|
336
|
+
data = series.asarray()
|
337
|
+
dims = tuple(series.axes)
|
338
|
+
coords = {}
|
339
|
+
# channel wavelengths
|
340
|
+
axis = dims.index('C')
|
341
|
+
wavelengths = lsminfo['ChannelWavelength'].mean(axis=1)
|
342
|
+
if wavelengths.size != data.shape[axis]:
|
343
|
+
raise ValueError(
|
344
|
+
f'{tif.filename!r} wavelengths do not match channel axis'
|
345
|
+
)
|
346
|
+
# stack may contain non-wavelength frame
|
347
|
+
indices = wavelengths > 0
|
348
|
+
wavelengths = wavelengths[indices]
|
349
|
+
if wavelengths.size < 3:
|
350
|
+
raise ValueError(
|
351
|
+
f'{tif.filename!r} does not contain hyperspectral image'
|
352
|
+
)
|
353
|
+
wavelengths *= 1e9
|
354
|
+
data = data.take(indices.nonzero()[0], axis=axis)
|
355
|
+
coords['C'] = wavelengths
|
356
|
+
# time stamps
|
357
|
+
if 'T' in dims:
|
358
|
+
coords['T'] = lsminfo['TimeStamps'] - lsminfo['TimeStamps'][0]
|
359
|
+
if coords['T'].size != data.shape[dims.index('T')]:
|
360
|
+
raise ValueError(
|
361
|
+
f'{tif.filename!r} timestamps do not match time axis'
|
362
|
+
)
|
363
|
+
# spatial coordinates
|
364
|
+
for ax in 'ZYX':
|
365
|
+
if ax in dims:
|
366
|
+
size = data.shape[dims.index(ax)]
|
367
|
+
coords[ax] = numpy.linspace(
|
368
|
+
lsminfo[f'Origin{ax}'],
|
369
|
+
size * lsminfo[f'VoxelSize{ax}'],
|
370
|
+
size,
|
371
|
+
endpoint=False,
|
372
|
+
dtype=numpy.float64,
|
373
|
+
)
|
374
|
+
metadata = xarray_metadata(series.axes, data.shape, filename, **coords)
|
375
|
+
|
376
|
+
from xarray import DataArray
|
377
|
+
|
378
|
+
return DataArray(data, **metadata)
|
379
|
+
|
380
|
+
|
381
|
+
def signal_from_imspector_tiff(
|
382
|
+
filename: str | PathLike[Any],
|
383
|
+
/,
|
384
|
+
) -> DataArray:
|
385
|
+
"""Return TCSPC histogram and metadata from ImSpector TIFF file.
|
386
|
+
|
387
|
+
Parameters
|
388
|
+
----------
|
389
|
+
filename : str or Path
|
390
|
+
Name of ImSpector FLIM TIFF file to read.
|
391
|
+
|
392
|
+
Returns
|
393
|
+
-------
|
394
|
+
xarray.DataArray
|
395
|
+
TCSPC histogram with :ref:`axes codes <axes>` ``'HTZYX'`` and
|
396
|
+
type ``uint16``.
|
397
|
+
|
398
|
+
- ``coords['H']``: delay-times of histogram bins in ns.
|
399
|
+
- ``attrs['frequency']``: repetition frequency in MHz.
|
400
|
+
|
401
|
+
Raises
|
402
|
+
------
|
403
|
+
tifffile.TiffFileError
|
404
|
+
File is not a TIFF file.
|
405
|
+
ValueError
|
406
|
+
File is not an ImSpector FLIM TIFF file.
|
407
|
+
|
408
|
+
Notes
|
409
|
+
-----
|
410
|
+
The implementation is based on the
|
411
|
+
`tifffile <https://github.com/cgohlke/tifffile/>`__ library.
|
412
|
+
|
413
|
+
Examples
|
414
|
+
--------
|
415
|
+
>>> signal = signal_from_imspector_tiff(fetch('Embryo.tif'))
|
416
|
+
>>> signal.values
|
417
|
+
array(...)
|
418
|
+
>>> signal.dtype
|
419
|
+
dtype('uint16')
|
420
|
+
>>> signal.shape
|
421
|
+
(56, 512, 512)
|
422
|
+
>>> signal.dims
|
423
|
+
('H', 'Y', 'X')
|
424
|
+
>>> signal.coords['H'].data # dtime bins
|
425
|
+
array([0, ..., 12.26])
|
426
|
+
>>> signal.attrs['frequency'] # doctest: +NUMBER
|
427
|
+
80.109
|
428
|
+
|
429
|
+
"""
|
430
|
+
import tifffile
|
431
|
+
|
432
|
+
with tifffile.TiffFile(filename) as tif:
|
433
|
+
tags = tif.pages.first.tags
|
434
|
+
omexml = tags.valueof(270, '')
|
435
|
+
make = tags.valueof(271, '')
|
436
|
+
|
437
|
+
if (
|
438
|
+
make != 'ImSpector'
|
439
|
+
or not omexml.startswith('<?xml version')
|
440
|
+
or len(tif.series) != 1
|
441
|
+
or not tif.is_ome
|
442
|
+
):
|
443
|
+
raise ValueError(f'{tif.filename!r} is not an ImSpector TIFF file')
|
444
|
+
|
445
|
+
series = tif.series[0]
|
446
|
+
ndim = series.ndim
|
447
|
+
axes = series.axes
|
448
|
+
shape = series.shape
|
449
|
+
|
450
|
+
if ndim < 3 or not axes.endswith('YX'):
|
451
|
+
raise ValueError(
|
452
|
+
f'{tif.filename!r} is not an ImSpector FLIM TIFF file'
|
453
|
+
)
|
454
|
+
|
455
|
+
data = series.asarray()
|
456
|
+
|
457
|
+
attrs: dict[str, Any] = {}
|
458
|
+
coords = {}
|
459
|
+
physical_size = {}
|
460
|
+
|
461
|
+
root = ElementTree.fromstring(omexml)
|
462
|
+
ns = {
|
463
|
+
'': 'http://www.openmicroscopy.org/Schemas/OME/2008-02',
|
464
|
+
'ca': 'http://www.openmicroscopy.org/Schemas/CA/2008-02',
|
465
|
+
}
|
466
|
+
|
467
|
+
description = root.find('.//Description', ns)
|
468
|
+
if (
|
469
|
+
description is not None
|
470
|
+
and description.text
|
471
|
+
and description.text != 'not_specified'
|
472
|
+
):
|
473
|
+
attrs['description'] = description.text
|
474
|
+
|
475
|
+
pixels = root.find('.//Image/Pixels', ns)
|
476
|
+
assert pixels is not None
|
477
|
+
for ax in 'TZYX':
|
478
|
+
attrib = 'TimeIncrement' if ax == 'T' else f'PhysicalSize{ax}'
|
479
|
+
if ax not in axes or attrib not in pixels.attrib:
|
480
|
+
continue
|
481
|
+
size = float(pixels.attrib[attrib]) * shape[axes.index(ax)]
|
482
|
+
physical_size[ax] = size
|
483
|
+
coords[ax] = numpy.linspace(
|
484
|
+
0.0,
|
485
|
+
size,
|
486
|
+
shape[axes.index(ax)],
|
487
|
+
endpoint=False,
|
488
|
+
dtype=numpy.float64,
|
489
|
+
)
|
490
|
+
|
491
|
+
axes_labels = root.find('.//Image/ca:CustomAttributes/AxesLabels', ns)
|
492
|
+
if (
|
493
|
+
axes_labels is None
|
494
|
+
or 'X' not in axes_labels.attrib
|
495
|
+
or 'TCSPC' not in axes_labels.attrib['X']
|
496
|
+
or 'FirstAxis' not in axes_labels.attrib
|
497
|
+
or 'SecondAxis' not in axes_labels.attrib
|
498
|
+
):
|
499
|
+
raise ValueError(
|
500
|
+
f'{tif.filename!r} is not an ImSpector FLIM TIFF file'
|
501
|
+
)
|
502
|
+
|
503
|
+
if axes_labels.attrib['FirstAxis'] == 'lifetime' or axes_labels.attrib[
|
504
|
+
'FirstAxis'
|
505
|
+
].endswith('TCSPC T'):
|
506
|
+
ax = axes[-3]
|
507
|
+
assert axes_labels.attrib['FirstAxis-Unit'] == 'ns'
|
508
|
+
elif ndim > 3 and (
|
509
|
+
axes_labels.attrib['SecondAxis'] == 'lifetime'
|
510
|
+
or axes_labels.attrib['SecondAxis'].endswith('TCSPC T')
|
511
|
+
):
|
512
|
+
ax = axes[-4]
|
513
|
+
assert axes_labels.attrib['SecondAxis-Unit'] == 'ns'
|
514
|
+
else:
|
515
|
+
raise ValueError(
|
516
|
+
f'{tif.filename!r} is not an ImSpector FLIM TIFF file'
|
517
|
+
)
|
518
|
+
axes = axes.replace(ax, 'H')
|
519
|
+
coords['H'] = coords[ax]
|
520
|
+
del coords[ax]
|
521
|
+
|
522
|
+
attrs['frequency'] = float(1000.0 / physical_size[ax])
|
523
|
+
|
524
|
+
metadata = xarray_metadata(axes, shape, filename, attrs=attrs, **coords)
|
525
|
+
|
526
|
+
from xarray import DataArray
|
527
|
+
|
528
|
+
return DataArray(data, **metadata)
|
529
|
+
|
530
|
+
|
531
|
+
def phasor_from_ifli(
|
532
|
+
filename: str | PathLike[Any],
|
533
|
+
/,
|
534
|
+
*,
|
535
|
+
channel: int | None = 0,
|
536
|
+
harmonic: int | Sequence[int] | Literal['all', 'any'] | str | None = None,
|
537
|
+
**kwargs: Any,
|
538
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], dict[str, Any]]:
|
539
|
+
"""Return phasor coordinates and metadata from ISS IFLI file.
|
540
|
+
|
541
|
+
ISS VistaVision IFLI files contain calibrated phasor coordinates for
|
542
|
+
possibly several positions, wavelengths, time points, channels, slices,
|
543
|
+
and frequencies from analog or digital frequency-domain fluorescence
|
544
|
+
lifetime measurements.
|
545
|
+
|
546
|
+
Parameters
|
547
|
+
----------
|
548
|
+
filename : str or Path
|
549
|
+
Name of ISS IFLI file to read.
|
550
|
+
channel : int, optional
|
551
|
+
Index of channel to return.
|
552
|
+
By default, return the first channel.
|
553
|
+
If None, return all channels.
|
554
|
+
harmonic : int, sequence of int, 'any', or 'all', optional
|
555
|
+
Harmonic(s) to return from file.
|
556
|
+
If None (default), return the first harmonic stored in file.
|
557
|
+
If `'all'`, return all harmonics of first frequency stored in file.
|
558
|
+
If `'any'`, return all frequencies as stored in file, not necessarily
|
559
|
+
harmonics of the first frequency.
|
560
|
+
If a list, the first axes of the returned `real` and `imag` arrays
|
561
|
+
contain specified harmonic(s).
|
562
|
+
If an integer, the returned `real` and `imag` arrays are single
|
563
|
+
harmonic and have the same shape as `mean`.
|
564
|
+
**kwargs
|
565
|
+
Additional arguments passed to :py:meth:`lfdfiles.VistaIfli.asarray`,
|
566
|
+
for example ``memmap=True``.
|
567
|
+
|
568
|
+
Returns
|
569
|
+
-------
|
570
|
+
mean : ndarray
|
571
|
+
Average intensity image.
|
572
|
+
May have up to 7 dimensions in ``'RETCZYX'`` order.
|
573
|
+
real : ndarray
|
574
|
+
Image of real component of phasor coordinates.
|
575
|
+
Same shape as `mean`, except it may have a harmonic/frequency
|
576
|
+
dimension prepended.
|
577
|
+
imag : ndarray
|
578
|
+
Image of imaginary component of phasor coordinates.
|
579
|
+
Same shape as `real`.
|
580
|
+
attrs : dict
|
581
|
+
Select metadata:
|
582
|
+
|
583
|
+
- ``'dims'`` (tuple of str):
|
584
|
+
:ref:`Axes codes <axes>` for `mean` image dimensions.
|
585
|
+
- ``'harmonic'`` (int or list of int):
|
586
|
+
Harmonic(s) present in `real` and `imag`.
|
587
|
+
If a scalar, `real` and `imag` are single harmonic and contain no
|
588
|
+
harmonic axes.
|
589
|
+
If a list, `real` and `imag` contain one or more harmonics in the
|
590
|
+
first axis.
|
591
|
+
- ``'frequency'`` (float):
|
592
|
+
Fundamental frequency of time-resolved phasor coordinates in MHz.
|
593
|
+
- ``'samples'`` (int):
|
594
|
+
Number of samples per frequency.
|
595
|
+
- ``'ifli_header'`` (dict):
|
596
|
+
Metadata from IFLI file header.
|
597
|
+
|
598
|
+
Raises
|
599
|
+
------
|
600
|
+
lfdfiles.LfdFileError
|
601
|
+
File is not an ISS IFLI file.
|
602
|
+
IndexError
|
603
|
+
Harmonic is not found in file.
|
604
|
+
|
605
|
+
Notes
|
606
|
+
-----
|
607
|
+
The implementation is based on the
|
608
|
+
`lfdfiles <https://github.com/cgohlke/lfdfiles/>`__ library.
|
609
|
+
|
610
|
+
Examples
|
611
|
+
--------
|
612
|
+
>>> mean, real, imag, attr = phasor_from_ifli(
|
613
|
+
... fetch('frequency_domain.ifli'), harmonic='all'
|
614
|
+
... )
|
615
|
+
>>> mean.shape
|
616
|
+
(256, 256)
|
617
|
+
>>> real.shape
|
618
|
+
(4, 256, 256)
|
619
|
+
>>> attr['dims']
|
620
|
+
('Y', 'X')
|
621
|
+
>>> attr['harmonic']
|
622
|
+
[1, 2, 3, 5]
|
623
|
+
>>> attr['frequency'] # doctest: +NUMBER
|
624
|
+
80.33
|
625
|
+
>>> attr['samples']
|
626
|
+
64
|
627
|
+
>>> attr['ifli_header']
|
628
|
+
{'Version': 16, ... 'ModFrequency': (...), 'RefLifetime': (2.5,), ...}
|
629
|
+
|
630
|
+
"""
|
631
|
+
import lfdfiles
|
632
|
+
|
633
|
+
with lfdfiles.VistaIfli(filename) as ifli:
|
634
|
+
assert ifli.axes is not None
|
635
|
+
data = ifli.asarray(**kwargs)
|
636
|
+
header = ifli.header
|
637
|
+
axes = ifli.axes
|
638
|
+
|
639
|
+
if channel is not None:
|
640
|
+
data = data[:, :, :, channel]
|
641
|
+
axes = axes[:3] + axes[4:]
|
642
|
+
|
643
|
+
shape, dims, _ = squeeze_dims(data.shape, axes, skip='YXF')
|
644
|
+
data = data.reshape(shape)
|
645
|
+
data = numpy.moveaxis(data, -2, 0) # move frequency to first axis
|
646
|
+
mean = data[..., 0].mean(axis=0) # average frequencies
|
647
|
+
real = data[..., 1].copy()
|
648
|
+
imag = data[..., 2].copy()
|
649
|
+
dims = dims[:-2]
|
650
|
+
del data
|
651
|
+
|
652
|
+
samples = header['HistogramResolution']
|
653
|
+
frequencies = header['ModFrequency']
|
654
|
+
frequency = frequencies[0]
|
655
|
+
harmonic_stored = [
|
656
|
+
(
|
657
|
+
int(round(f / frequency))
|
658
|
+
if (0.99 < f / frequency % 1.0) < 1.01
|
659
|
+
else None
|
660
|
+
)
|
661
|
+
for f in frequencies
|
662
|
+
]
|
663
|
+
|
664
|
+
index: int | list[int]
|
665
|
+
if harmonic is None:
|
666
|
+
# return first harmonic in file
|
667
|
+
keepdims = False
|
668
|
+
harmonic = [1]
|
669
|
+
index = [0]
|
670
|
+
elif isinstance(harmonic, str) and harmonic in {'all', 'any'}:
|
671
|
+
keepdims = True
|
672
|
+
if harmonic == 'any':
|
673
|
+
# return any frequency
|
674
|
+
harmonic = [
|
675
|
+
(frequencies[i] / frequency if h is None else h)
|
676
|
+
for i, h in enumerate(harmonic_stored)
|
677
|
+
]
|
678
|
+
index = list(range(len(harmonic_stored)))
|
679
|
+
else:
|
680
|
+
# return only harmonics of first frequency
|
681
|
+
harmonic = [h for h in harmonic_stored if h is not None]
|
682
|
+
index = [i for i, h in enumerate(harmonic_stored) if h is not None]
|
683
|
+
else:
|
684
|
+
# return specified harmonics
|
685
|
+
harmonic, keepdims = parse_harmonic(
|
686
|
+
harmonic, max(h for h in harmonic_stored if h is not None)
|
687
|
+
)
|
688
|
+
try:
|
689
|
+
index = [harmonic_stored.index(h) for h in harmonic]
|
690
|
+
except ValueError as exc:
|
691
|
+
raise IndexError('harmonic not found') from exc
|
692
|
+
|
693
|
+
real = real[index]
|
694
|
+
imag = imag[index]
|
695
|
+
if not keepdims:
|
696
|
+
real = real[0]
|
697
|
+
imag = imag[0]
|
698
|
+
|
699
|
+
attrs = {
|
700
|
+
'dims': tuple(dims),
|
701
|
+
'harmonic': harmonic,
|
702
|
+
'frequency': frequency * 1e-6,
|
703
|
+
'samples': samples,
|
704
|
+
'ifli_header': header,
|
705
|
+
}
|
706
|
+
|
707
|
+
return mean, real, imag, attrs
|
708
|
+
|
709
|
+
|
710
|
+
def signal_from_flif(
|
711
|
+
filename: str | PathLike[Any],
|
712
|
+
/,
|
713
|
+
) -> DataArray:
|
714
|
+
"""Return phase images and metadata from FlimFast FLIF file.
|
715
|
+
|
716
|
+
FlimFast FLIF files contain phase images and metadata from full-field,
|
717
|
+
frequency-domain fluorescence lifetime measurements.
|
718
|
+
|
719
|
+
Parameters
|
720
|
+
----------
|
721
|
+
filename : str or Path
|
722
|
+
Name of FlimFast FLIF file to read.
|
723
|
+
|
724
|
+
Returns
|
725
|
+
-------
|
726
|
+
xarray.DataArray
|
727
|
+
Phase images with :ref:`axes codes <axes>` ``'THYX'`` and
|
728
|
+
type ``uint16``:
|
729
|
+
|
730
|
+
- ``coords['H']``: phases in radians.
|
731
|
+
- ``attrs['frequency']``: repetition frequency in MHz.
|
732
|
+
- ``attrs['ref_phase']``: measured phase of reference.
|
733
|
+
- ``attrs['ref_mod']``: measured modulation of reference.
|
734
|
+
- ``attrs['ref_tauphase']``: lifetime from phase of reference in ns.
|
735
|
+
- ``attrs['ref_taumod']``: lifetime from modulation of reference in ns.
|
736
|
+
|
737
|
+
Raises
|
738
|
+
------
|
739
|
+
lfdfiles.LfdFileError
|
740
|
+
File is not a FlimFast FLIF file.
|
741
|
+
|
742
|
+
Notes
|
743
|
+
-----
|
744
|
+
The implementation is based on the
|
745
|
+
`lfdfiles <https://github.com/cgohlke/lfdfiles/>`__ library.
|
746
|
+
|
747
|
+
Examples
|
748
|
+
--------
|
749
|
+
>>> signal = signal_from_flif(fetch('flimfast.flif'))
|
750
|
+
>>> signal.values
|
751
|
+
array(...)
|
752
|
+
>>> signal.dtype
|
753
|
+
dtype('uint16')
|
754
|
+
>>> signal.shape
|
755
|
+
(32, 220, 300)
|
756
|
+
>>> signal.dims
|
757
|
+
('H', 'Y', 'X')
|
758
|
+
>>> signal.coords['H'].data
|
759
|
+
array([0, ..., 6.087], dtype=float32)
|
760
|
+
>>> signal.attrs['frequency'] # doctest: +NUMBER
|
761
|
+
80.65
|
762
|
+
|
763
|
+
"""
|
764
|
+
import lfdfiles
|
765
|
+
|
766
|
+
with lfdfiles.FlimfastFlif(filename) as flif:
|
767
|
+
nphases = int(flif.header.phases)
|
768
|
+
data = flif.asarray()
|
769
|
+
if data.shape[0] < nphases:
|
770
|
+
raise ValueError(f'measured phases {data.shape[0]} < {nphases=}')
|
771
|
+
if data.shape[0] % nphases != 0:
|
772
|
+
data = data[: (data.shape[0] // nphases) * nphases]
|
773
|
+
data = data.reshape(-1, nphases, data.shape[1], data.shape[2])
|
774
|
+
if data.shape[0] == 1:
|
775
|
+
data = data[0]
|
776
|
+
axes = 'HYX'
|
777
|
+
else:
|
778
|
+
axes = 'THYX'
|
779
|
+
# TODO: check if phases are ordered
|
780
|
+
phases = numpy.radians(flif.records['phase'][:nphases])
|
781
|
+
metadata = xarray_metadata(axes, data.shape, H=phases)
|
782
|
+
attrs = metadata['attrs']
|
783
|
+
attrs['frequency'] = float(flif.header.frequency)
|
784
|
+
attrs['ref_phase'] = float(flif.header.measured_phase)
|
785
|
+
attrs['ref_mod'] = float(flif.header.measured_mod)
|
786
|
+
attrs['ref_tauphase'] = float(flif.header.ref_tauphase)
|
787
|
+
attrs['ref_taumod'] = float(flif.header.ref_taumod)
|
788
|
+
|
789
|
+
from xarray import DataArray
|
790
|
+
|
791
|
+
return DataArray(data, **metadata)
|
792
|
+
|
793
|
+
|
794
|
+
def signal_from_pqbin(
|
795
|
+
filename: str | PathLike[Any],
|
796
|
+
/,
|
797
|
+
) -> DataArray:
|
798
|
+
"""Return TCSPC histogram and metadata from PicoQuant BIN file.
|
799
|
+
|
800
|
+
PicoQuant BIN files contain TCSPC histograms with limited metadata.
|
801
|
+
|
802
|
+
Parameters
|
803
|
+
----------
|
804
|
+
filename : str or Path
|
805
|
+
Name of PicoQuant BIN file to read.
|
806
|
+
|
807
|
+
Returns
|
808
|
+
-------
|
809
|
+
xarray.DataArray
|
810
|
+
TCSPC histogram with :ref:`axes codes <axes>` ``'YXH'``,
|
811
|
+
and type ``uint32``.
|
812
|
+
|
813
|
+
- ``coords['H']``: delay-times of histogram bins in ns.
|
814
|
+
- ``attrs['frequency']``: repetition frequency in MHz.
|
815
|
+
This assumes that the histogram contains exactly one period.
|
816
|
+
|
817
|
+
Raises
|
818
|
+
------
|
819
|
+
ValueError
|
820
|
+
File is not a PicoQuant BIN file.
|
821
|
+
|
822
|
+
Examples
|
823
|
+
--------
|
824
|
+
>>> signal = signal_from_pqbin('picoquant.bin') # doctest: +SKIP
|
825
|
+
>>> signal.values # doctest: +SKIP
|
826
|
+
array(...)
|
827
|
+
>>> signal.dtype # doctest: +SKIP
|
828
|
+
dtype('uint32')
|
829
|
+
>>> signal.shape # doctest: +SKIP
|
830
|
+
(256, 256, 2000)
|
831
|
+
>>> signal.dims # doctest: +SKIP
|
832
|
+
('Y', 'X', 'H')
|
833
|
+
>>> signal.coords['H'].data # doctest: +SKIP
|
834
|
+
array([0, ..., 49.975])
|
835
|
+
>>> signal.attrs['frequency'] # doctest: +SKIP
|
836
|
+
19.99
|
837
|
+
|
838
|
+
"""
|
839
|
+
with open(filename, 'rb') as fh:
|
840
|
+
header = fh.read(20)
|
841
|
+
if len(header) != 20:
|
842
|
+
raise ValueError(
|
843
|
+
f'invalid PicoQuant BIN header length {len(header)} != 20'
|
844
|
+
)
|
845
|
+
(size_x, size_y, pixel_resolution, size_h, tcspc_resolution) = (
|
846
|
+
struct.unpack('<IIfIf', header)
|
847
|
+
)
|
848
|
+
size = size_y * size_x * size_h * 4
|
849
|
+
|
850
|
+
# check the header values against arbitrary but reasonable limits
|
851
|
+
# to detect invalid files and prevent memory errors
|
852
|
+
if (
|
853
|
+
size <= 0
|
854
|
+
or size > 2**35 - 1 # 32 GiB
|
855
|
+
or size_x > 16384
|
856
|
+
or size_y > 16384
|
857
|
+
or size_h > 16384
|
858
|
+
or pixel_resolution < 0.0
|
859
|
+
or pixel_resolution > 1.0
|
860
|
+
or tcspc_resolution <= 0.0
|
861
|
+
or tcspc_resolution > 1.0
|
862
|
+
):
|
863
|
+
raise ValueError('invalid PicoQuant BIN file header')
|
864
|
+
|
865
|
+
shape = size_y, size_x, size_h
|
866
|
+
data = numpy.empty(shape, '<u4')
|
867
|
+
if fh.readinto(data) != size:
|
868
|
+
raise ValueError('invalid PicoQuant BIN data size')
|
869
|
+
|
870
|
+
metadata = xarray_metadata(
|
871
|
+
('Y', 'X', 'H'),
|
872
|
+
shape,
|
873
|
+
filename,
|
874
|
+
attrs={
|
875
|
+
'frequency': 1000.0 / (size_h * tcspc_resolution), # MHz
|
876
|
+
'pixel_resolution': pixel_resolution, # um
|
877
|
+
'tcspc_resolution': tcspc_resolution, # ns
|
878
|
+
},
|
879
|
+
Y=numpy.linspace(
|
880
|
+
0, size_y * pixel_resolution * 1e-6, size_y, endpoint=False
|
881
|
+
),
|
882
|
+
X=numpy.linspace(
|
883
|
+
0, size_x * pixel_resolution * 1e-6, size_x, endpoint=False
|
884
|
+
),
|
885
|
+
H=numpy.linspace(0, size_h * tcspc_resolution, size_h, endpoint=False),
|
886
|
+
)
|
887
|
+
|
888
|
+
from xarray import DataArray
|
889
|
+
|
890
|
+
return DataArray(data, **metadata)
|