phasorpy 0.5__cp313-cp313-win_arm64.whl → 0.7__cp313-cp313-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.cp313-win_arm64.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.py
DELETED
@@ -1,2655 +0,0 @@
|
|
1
|
-
"""Read and write time-resolved and hyperspectral image file formats.
|
2
|
-
|
3
|
-
The ``phasorpy.io`` module provides functions to:
|
4
|
-
|
5
|
-
- read time-resolved and hyperspectral signals, as well as metadata from
|
6
|
-
many file formats used in bio-imaging:
|
7
|
-
|
8
|
-
- :py:func:`signal_from_lif` - Leica LIF and XLEF
|
9
|
-
- :py:func:`signal_from_lsm` - Zeiss LSM
|
10
|
-
- :py:func:`signal_from_ptu` - PicoQuant PTU
|
11
|
-
- :py:func:`signal_from_sdt` - Becker & Hickl SDT
|
12
|
-
- :py:func:`signal_from_fbd` - FLIMbox FBD
|
13
|
-
- :py:func:`signal_from_flimlabs_json` - FLIM LABS JSON
|
14
|
-
- :py:func:`signal_from_imspector_tiff` - ImSpector FLIM TIFF
|
15
|
-
- :py:func:`signal_from_flif` - FlimFast FLIF
|
16
|
-
- :py:func:`signal_from_b64` - SimFCS B64
|
17
|
-
- :py:func:`signal_from_z64` - SimFCS Z64
|
18
|
-
- :py:func:`signal_from_bhz` - SimFCS BHZ
|
19
|
-
- :py:func:`signal_from_bh` - SimFCS B&H
|
20
|
-
|
21
|
-
- read phasor coordinates, lifetime images, and metadata from
|
22
|
-
specialized file formats:
|
23
|
-
|
24
|
-
- :py:func:`phasor_from_ometiff` - PhasorPy OME-TIFF
|
25
|
-
- :py:func:`phasor_from_ifli` - ISS IFLI
|
26
|
-
- :py:func:`phasor_from_lif` - Leica LIF and XLEF
|
27
|
-
- :py:func:`phasor_from_flimlabs_json` - FLIM LABS JSON
|
28
|
-
- :py:func:`phasor_from_simfcs_referenced` - SimFCS REF and R64
|
29
|
-
- :py:func:`lifetime_from_lif` - Leica LIF and XLEF
|
30
|
-
|
31
|
-
- write phasor coordinate images to OME-TIFF and SimFCS file formats:
|
32
|
-
|
33
|
-
- :py:func:`phasor_to_ometiff`
|
34
|
-
- :py:func:`phasor_to_simfcs_referenced`
|
35
|
-
|
36
|
-
Support for other file formats is being considered:
|
37
|
-
|
38
|
-
- OME-TIFF
|
39
|
-
- Zeiss CZI
|
40
|
-
- Nikon ND2
|
41
|
-
- Olympus OIB/OIF
|
42
|
-
- Olympus OIR
|
43
|
-
|
44
|
-
The functions are implemented as minimal wrappers around specialized
|
45
|
-
third-party file reader libraries, currently
|
46
|
-
`tifffile <https://github.com/cgohlke/tifffile>`_,
|
47
|
-
`ptufile <https://github.com/cgohlke/ptufile>`_,
|
48
|
-
`liffile <https://github.com/cgohlke/liffile>`_,
|
49
|
-
`sdtfile <https://github.com/cgohlke/sdtfile>`_, and
|
50
|
-
`lfdfiles <https://github.com/cgohlke/lfdfiles>`_.
|
51
|
-
For advanced or unsupported use cases, consider using these libraries directly.
|
52
|
-
|
53
|
-
The signal-reading functions typically have the following signature::
|
54
|
-
|
55
|
-
signal_from_ext(
|
56
|
-
filename: str | PathLike,
|
57
|
-
/,
|
58
|
-
**kwargs
|
59
|
-
): -> xarray.DataArray
|
60
|
-
|
61
|
-
where ``ext`` indicates the file format and ``kwargs`` are optional arguments
|
62
|
-
passed to the underlying file reader library or used to select which data is
|
63
|
-
returned. The returned `xarray.DataArray
|
64
|
-
<https://docs.xarray.dev/en/stable/user-guide/data-structures.html>`_
|
65
|
-
contains an N-dimensional array with labeled coordinates, dimensions, and
|
66
|
-
attributes:
|
67
|
-
|
68
|
-
- ``data`` or ``values`` (*array_like*)
|
69
|
-
|
70
|
-
Numpy array or array-like holding the array's values.
|
71
|
-
|
72
|
-
- ``dims`` (*tuple of str*)
|
73
|
-
|
74
|
-
:ref:`Axes character codes <axes>` for each dimension in ``data``.
|
75
|
-
For example, ``('T', 'C', 'Y', 'X')`` defines the dimension order in a
|
76
|
-
4-dimensional array of a time-series of multi-channel images.
|
77
|
-
|
78
|
-
- ``coords`` (*dict_like[str, array_like]*)
|
79
|
-
|
80
|
-
Coordinate arrays labelling each point in the data array.
|
81
|
-
The keys are :ref:`axes character codes <axes>`.
|
82
|
-
Values are 1-dimensional arrays of numbers or strings.
|
83
|
-
For example, ``coords['C']`` could be an array of emission wavelengths.
|
84
|
-
|
85
|
-
- ``attrs`` (*dict[str, Any]*)
|
86
|
-
|
87
|
-
Arbitrary metadata such as measurement or calibration parameters required to
|
88
|
-
interpret the data values.
|
89
|
-
For example, the laser repetition frequency of a time-resolved measurement.
|
90
|
-
|
91
|
-
.. _axes:
|
92
|
-
|
93
|
-
Axes character codes from the OME model and tifffile library are used as
|
94
|
-
``dims`` items and ``coords`` keys:
|
95
|
-
|
96
|
-
- ``'X'`` : width (OME)
|
97
|
-
- ``'Y'`` : height (OME)
|
98
|
-
- ``'Z'`` : depth (OME)
|
99
|
-
- ``'S'`` : sample (color components or phasor coordinates)
|
100
|
-
- ``'I'`` : sequence (of images, frames, or planes)
|
101
|
-
- ``'T'`` : time (OME)
|
102
|
-
- ``'C'`` : channel (OME. Acquisition path or emission wavelength)
|
103
|
-
- ``'A'`` : angle (OME)
|
104
|
-
- ``'P'`` : phase (OME. In LSM, ``'P'`` maps to position)
|
105
|
-
- ``'R'`` : tile (OME. Region, position, or mosaic)
|
106
|
-
- ``'H'`` : lifetime histogram (OME)
|
107
|
-
- ``'E'`` : lambda (OME. Excitation wavelength)
|
108
|
-
- ``'F'`` : frequency (ISS)
|
109
|
-
- ``'Q'`` : other (OME. Harmonics in PhasorPy TIFF)
|
110
|
-
- ``'L'`` : exposure (FluoView)
|
111
|
-
- ``'V'`` : event (FluoView)
|
112
|
-
- ``'M'`` : mosaic (LSM 6)
|
113
|
-
- ``'J'`` : column (NDTiff)
|
114
|
-
- ``'K'`` : row (NDTiff)
|
115
|
-
|
116
|
-
"""
|
117
|
-
|
118
|
-
from __future__ import annotations
|
119
|
-
|
120
|
-
__all__ = [
|
121
|
-
'lifetime_from_lif',
|
122
|
-
'phasor_from_flimlabs_json',
|
123
|
-
'phasor_from_ifli',
|
124
|
-
'phasor_from_lif',
|
125
|
-
'phasor_from_ometiff',
|
126
|
-
'phasor_from_simfcs_referenced',
|
127
|
-
'phasor_to_ometiff',
|
128
|
-
'phasor_to_simfcs_referenced',
|
129
|
-
'signal_from_b64',
|
130
|
-
'signal_from_bh',
|
131
|
-
'signal_from_bhz',
|
132
|
-
# 'signal_from_czi',
|
133
|
-
'signal_from_fbd',
|
134
|
-
'signal_from_flif',
|
135
|
-
'signal_from_flimlabs_json',
|
136
|
-
'signal_from_imspector_tiff',
|
137
|
-
'signal_from_lif',
|
138
|
-
'signal_from_lsm',
|
139
|
-
# 'signal_from_nd2',
|
140
|
-
# 'signal_from_oif',
|
141
|
-
# 'signal_from_oir',
|
142
|
-
# 'signal_from_ometiff',
|
143
|
-
'signal_from_ptu',
|
144
|
-
'signal_from_sdt',
|
145
|
-
'signal_from_z64',
|
146
|
-
'_squeeze_dims',
|
147
|
-
]
|
148
|
-
|
149
|
-
import logging
|
150
|
-
import os
|
151
|
-
import re
|
152
|
-
import struct
|
153
|
-
import zlib
|
154
|
-
from typing import TYPE_CHECKING
|
155
|
-
|
156
|
-
from ._utils import chunk_iter, parse_harmonic
|
157
|
-
from .phasor import phasor_from_polar, phasor_to_polar
|
158
|
-
|
159
|
-
if TYPE_CHECKING:
|
160
|
-
from ._typing import (
|
161
|
-
Any,
|
162
|
-
ArrayLike,
|
163
|
-
Container,
|
164
|
-
DataArray,
|
165
|
-
DTypeLike,
|
166
|
-
EllipsisType,
|
167
|
-
Literal,
|
168
|
-
NDArray,
|
169
|
-
PathLike,
|
170
|
-
Sequence,
|
171
|
-
)
|
172
|
-
|
173
|
-
import numpy
|
174
|
-
|
175
|
-
logger = logging.getLogger(__name__)
|
176
|
-
|
177
|
-
|
178
|
-
def phasor_to_ometiff(
|
179
|
-
filename: str | PathLike[Any],
|
180
|
-
mean: ArrayLike,
|
181
|
-
real: ArrayLike,
|
182
|
-
imag: ArrayLike,
|
183
|
-
/,
|
184
|
-
*,
|
185
|
-
frequency: float | None = None,
|
186
|
-
harmonic: int | Sequence[int] | None = None,
|
187
|
-
dims: Sequence[str] | None = None,
|
188
|
-
dtype: DTypeLike | None = None,
|
189
|
-
description: str | None = None,
|
190
|
-
**kwargs: Any,
|
191
|
-
) -> None:
|
192
|
-
"""Write phasor coordinate images and metadata to OME-TIFF file.
|
193
|
-
|
194
|
-
The OME-TIFF format is compatible with Bio-Formats and Fiji.
|
195
|
-
|
196
|
-
By default, write phasor coordinates as single precision floating point
|
197
|
-
values to separate image series.
|
198
|
-
Write images larger than (1024, 1024) pixels as (256, 256) tiles, datasets
|
199
|
-
larger than 2 GB as BigTIFF, and datasets larger than 8 KB using
|
200
|
-
zlib compression.
|
201
|
-
|
202
|
-
This file format is experimental and might be incompatible with future
|
203
|
-
versions of this library. It is intended for temporarily exchanging
|
204
|
-
phasor coordinates with other software, not as a long-term storage
|
205
|
-
solution.
|
206
|
-
|
207
|
-
Parameters
|
208
|
-
----------
|
209
|
-
filename : str or Path
|
210
|
-
Name of PhasorPy OME-TIFF file to write.
|
211
|
-
mean : array_like
|
212
|
-
Average intensity image. Write to an image series named 'Phasor mean'.
|
213
|
-
real : array_like
|
214
|
-
Image of real component of phasor coordinates.
|
215
|
-
Multiple harmonics, if any, must be in the first dimension.
|
216
|
-
Write to image series named 'Phasor real'.
|
217
|
-
imag : array_like
|
218
|
-
Image of imaginary component of phasor coordinates.
|
219
|
-
Multiple harmonics, if any, must be in the first dimension.
|
220
|
-
Write to image series named 'Phasor imag'.
|
221
|
-
frequency : float, optional
|
222
|
-
Fundamental frequency of time-resolved phasor coordinates.
|
223
|
-
Usually in units of MHz.
|
224
|
-
Write to image series named 'Phasor frequency'.
|
225
|
-
harmonic : int or sequence of int, optional
|
226
|
-
Harmonics present in the first dimension of `real` and `imag`, if any.
|
227
|
-
Write to image series named 'Phasor harmonic'.
|
228
|
-
It is only needed if harmonics are not starting at and increasing by
|
229
|
-
one.
|
230
|
-
dims : sequence of str, optional
|
231
|
-
Character codes for `mean` image dimensions.
|
232
|
-
By default, the last dimensions are assumed to be 'TZCYX'.
|
233
|
-
If harmonics are present in `real` and `imag`, an "other" (``Q``)
|
234
|
-
dimension is prepended to axes for those arrays.
|
235
|
-
Refer to the OME-TIFF model for allowed axes and their order.
|
236
|
-
dtype : dtype-like, optional
|
237
|
-
Floating point data type used to store phasor coordinates.
|
238
|
-
The default is ``float32``, which has 6 digits of precision
|
239
|
-
and maximizes compatibility with other software.
|
240
|
-
description : str, optional
|
241
|
-
Plain-text description of dataset. Write as OME dataset description.
|
242
|
-
**kwargs
|
243
|
-
Additional arguments passed to :py:class:`tifffile.TiffWriter` and
|
244
|
-
:py:meth:`tifffile.TiffWriter.write`.
|
245
|
-
For example, ``compression=None`` writes image data uncompressed.
|
246
|
-
|
247
|
-
See Also
|
248
|
-
--------
|
249
|
-
phasorpy.io.phasor_from_ometiff
|
250
|
-
|
251
|
-
Notes
|
252
|
-
-----
|
253
|
-
Scalar or one-dimensional phasor coordinate arrays are written as images.
|
254
|
-
|
255
|
-
The OME-TIFF format is specified in the
|
256
|
-
`OME Data Model and File Formats Documentation
|
257
|
-
<https://ome-model.readthedocs.io/>`_.
|
258
|
-
|
259
|
-
The `6D, 7D and 8D storage
|
260
|
-
<https://ome-model.readthedocs.io/en/latest/developers/6d-7d-and-8d-storage.html>`_
|
261
|
-
extension is used to store multi-harmonic phasor coordinates.
|
262
|
-
The modulo type for the first, harmonic dimension is "other".
|
263
|
-
|
264
|
-
The implementation is based on the
|
265
|
-
`tifffile <https://github.com/cgohlke/tifffile/>`__ library.
|
266
|
-
|
267
|
-
Examples
|
268
|
-
--------
|
269
|
-
>>> mean, real, imag = numpy.random.rand(3, 32, 32, 32)
|
270
|
-
>>> phasor_to_ometiff(
|
271
|
-
... '_phasorpy.ome.tif', mean, real, imag, dims='ZYX', frequency=80.0
|
272
|
-
... )
|
273
|
-
|
274
|
-
"""
|
275
|
-
import tifffile
|
276
|
-
|
277
|
-
from .version import __version__
|
278
|
-
|
279
|
-
if dtype is None:
|
280
|
-
dtype = numpy.float32
|
281
|
-
dtype = numpy.dtype(dtype)
|
282
|
-
if dtype.kind != 'f':
|
283
|
-
raise ValueError(f'{dtype=} not a floating point type')
|
284
|
-
|
285
|
-
mean = numpy.asarray(mean, dtype)
|
286
|
-
real = numpy.asarray(real, dtype)
|
287
|
-
imag = numpy.asarray(imag, dtype)
|
288
|
-
datasize = mean.nbytes + real.nbytes + imag.nbytes
|
289
|
-
|
290
|
-
if real.shape != imag.shape:
|
291
|
-
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
292
|
-
if mean.shape != real.shape[-mean.ndim :]:
|
293
|
-
raise ValueError(f'{mean.shape=} != {real.shape[-mean.ndim:]=}')
|
294
|
-
has_harmonic_dim = real.ndim == mean.ndim + 1
|
295
|
-
if mean.ndim == real.ndim or real.ndim == 0:
|
296
|
-
nharmonic = 1
|
297
|
-
else:
|
298
|
-
nharmonic = real.shape[0]
|
299
|
-
|
300
|
-
if mean.ndim < 2:
|
301
|
-
# not an image
|
302
|
-
mean = mean.reshape(1, -1)
|
303
|
-
if has_harmonic_dim:
|
304
|
-
real = real.reshape(real.shape[0], 1, -1)
|
305
|
-
imag = imag.reshape(imag.shape[0], 1, -1)
|
306
|
-
else:
|
307
|
-
real = real.reshape(1, -1)
|
308
|
-
imag = imag.reshape(1, -1)
|
309
|
-
|
310
|
-
if harmonic is not None:
|
311
|
-
harmonic, _ = parse_harmonic(harmonic)
|
312
|
-
if len(harmonic) != nharmonic:
|
313
|
-
raise ValueError('invalid harmonic')
|
314
|
-
|
315
|
-
if frequency is not None:
|
316
|
-
frequency_array = numpy.atleast_2d(frequency).astype(numpy.float64)
|
317
|
-
if frequency_array.size > 1:
|
318
|
-
raise ValueError('frequency must be scalar')
|
319
|
-
|
320
|
-
axes = 'TZCYX'[-mean.ndim :] if dims is None else ''.join(tuple(dims))
|
321
|
-
if len(axes) != mean.ndim:
|
322
|
-
raise ValueError(f'{axes=} does not match {mean.ndim=}')
|
323
|
-
axes_phasor = axes if mean.ndim == real.ndim else 'Q' + axes
|
324
|
-
|
325
|
-
if 'photometric' not in kwargs:
|
326
|
-
kwargs['photometric'] = 'minisblack'
|
327
|
-
if 'compression' not in kwargs and datasize > 8192:
|
328
|
-
kwargs['compression'] = 'zlib'
|
329
|
-
if 'tile' not in kwargs and 'rowsperstrip' not in kwargs:
|
330
|
-
if (
|
331
|
-
axes.endswith('YX')
|
332
|
-
and mean.shape[-1] > 1024
|
333
|
-
and mean.shape[-2] > 1024
|
334
|
-
):
|
335
|
-
kwargs['tile'] = (256, 256)
|
336
|
-
|
337
|
-
mode = kwargs.pop('mode', None)
|
338
|
-
bigtiff = kwargs.pop('bigtiff', None)
|
339
|
-
if bigtiff is None:
|
340
|
-
bigtiff = datasize > 2**31
|
341
|
-
|
342
|
-
metadata = kwargs.pop('metadata', {})
|
343
|
-
if 'Creator' not in metadata:
|
344
|
-
metadata['Creator'] = f'PhasorPy {__version__}'
|
345
|
-
|
346
|
-
dataset = metadata.pop('Dataset', {})
|
347
|
-
if 'Name' not in dataset:
|
348
|
-
dataset['Name'] = 'Phasor'
|
349
|
-
if description:
|
350
|
-
dataset['Description'] = description
|
351
|
-
metadata['Dataset'] = dataset
|
352
|
-
|
353
|
-
if has_harmonic_dim:
|
354
|
-
metadata['TypeDescription'] = {'Q': 'Phasor harmonics'}
|
355
|
-
|
356
|
-
with tifffile.TiffWriter(
|
357
|
-
filename, bigtiff=bigtiff, mode=mode, ome=True
|
358
|
-
) as tif:
|
359
|
-
metadata['Name'] = 'Phasor mean'
|
360
|
-
metadata['axes'] = axes
|
361
|
-
tif.write(mean, metadata=metadata, **kwargs)
|
362
|
-
del metadata['Dataset']
|
363
|
-
|
364
|
-
metadata['Name'] = 'Phasor real'
|
365
|
-
metadata['axes'] = axes_phasor
|
366
|
-
tif.write(real, metadata=metadata, **kwargs)
|
367
|
-
|
368
|
-
metadata['Name'] = 'Phasor imag'
|
369
|
-
tif.write(imag, metadata=metadata, **kwargs)
|
370
|
-
|
371
|
-
if frequency is not None:
|
372
|
-
tif.write(frequency_array, metadata={'Name': 'Phasor frequency'})
|
373
|
-
|
374
|
-
if harmonic is not None:
|
375
|
-
tif.write(
|
376
|
-
numpy.atleast_2d(harmonic).astype(numpy.uint32),
|
377
|
-
metadata={'Name': 'Phasor harmonic'},
|
378
|
-
)
|
379
|
-
|
380
|
-
|
381
|
-
def phasor_from_ometiff(
|
382
|
-
filename: str | PathLike[Any],
|
383
|
-
/,
|
384
|
-
*,
|
385
|
-
harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
|
386
|
-
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], dict[str, Any]]:
|
387
|
-
"""Return phasor coordinates and metadata from PhasorPy OME-TIFF file.
|
388
|
-
|
389
|
-
PhasorPy OME-TIFF files contain phasor mean intensity, real and imaginary
|
390
|
-
components, along with frequency and harmonic information.
|
391
|
-
|
392
|
-
Parameters
|
393
|
-
----------
|
394
|
-
filename : str or Path
|
395
|
-
Name of PhasorPy OME-TIFF file to read.
|
396
|
-
harmonic : int, sequence of int, or 'all', optional
|
397
|
-
Harmonic(s) to return from file.
|
398
|
-
If None (default), return the first harmonic stored in the file.
|
399
|
-
If `'all'`, return all harmonics as stored in file.
|
400
|
-
If a list, the first axes of the returned `real` and `imag` arrays
|
401
|
-
contain specified harmonic(s).
|
402
|
-
If an integer, the returned `real` and `imag` arrays are single
|
403
|
-
harmonic and have the same shape as `mean`.
|
404
|
-
|
405
|
-
Returns
|
406
|
-
-------
|
407
|
-
mean : ndarray
|
408
|
-
Average intensity image.
|
409
|
-
real : ndarray
|
410
|
-
Image of real component of phasor coordinates.
|
411
|
-
imag : ndarray
|
412
|
-
Image of imaginary component of phasor coordinates.
|
413
|
-
attrs : dict
|
414
|
-
Select metadata:
|
415
|
-
|
416
|
-
- ``'dims'`` (tuple of str):
|
417
|
-
:ref:`Axes codes <axes>` for `mean` image dimensions.
|
418
|
-
- ``'harmonic'`` (int or list of int):
|
419
|
-
Harmonic(s) present in `real` and `imag`.
|
420
|
-
If a scalar, `real` and `imag` are single harmonic and contain no
|
421
|
-
harmonic axes.
|
422
|
-
If a list, `real` and `imag` contain one or more harmonics in the
|
423
|
-
first axis.
|
424
|
-
- ``'frequency'`` (float, optional):
|
425
|
-
Fundamental frequency of time-resolved phasor coordinates.
|
426
|
-
Usually in units of MHz.
|
427
|
-
- ``'description'`` (str, optional):
|
428
|
-
OME dataset plain-text description.
|
429
|
-
|
430
|
-
Raises
|
431
|
-
------
|
432
|
-
tifffile.TiffFileError
|
433
|
-
File is not a TIFF file.
|
434
|
-
ValueError
|
435
|
-
File is not an OME-TIFF containing phasor coordinates.
|
436
|
-
IndexError
|
437
|
-
Harmonic is not found in file.
|
438
|
-
|
439
|
-
See Also
|
440
|
-
--------
|
441
|
-
phasorpy.io.phasor_to_ometiff
|
442
|
-
|
443
|
-
Notes
|
444
|
-
-----
|
445
|
-
Scalar or one-dimensional phasor coordinates stored in the file are
|
446
|
-
returned as two-dimensional images (three-dimensional if multiple
|
447
|
-
harmonics are present).
|
448
|
-
|
449
|
-
The implementation is based on the
|
450
|
-
`tifffile <https://github.com/cgohlke/tifffile/>`__ library.
|
451
|
-
|
452
|
-
Examples
|
453
|
-
--------
|
454
|
-
>>> mean, real, imag = numpy.random.rand(3, 32, 32, 32)
|
455
|
-
>>> phasor_to_ometiff(
|
456
|
-
... '_phasorpy.ome.tif', mean, real, imag, dims='ZYX', frequency=80.0
|
457
|
-
... )
|
458
|
-
>>> mean, real, imag, attrs = phasor_from_ometiff('_phasorpy.ome.tif')
|
459
|
-
>>> mean
|
460
|
-
array(...)
|
461
|
-
>>> mean.dtype
|
462
|
-
dtype('float32')
|
463
|
-
>>> mean.shape
|
464
|
-
(32, 32, 32)
|
465
|
-
>>> attrs['dims']
|
466
|
-
('Z', 'Y', 'X')
|
467
|
-
>>> attrs['frequency']
|
468
|
-
80.0
|
469
|
-
>>> attrs['harmonic']
|
470
|
-
1
|
471
|
-
|
472
|
-
"""
|
473
|
-
import tifffile
|
474
|
-
|
475
|
-
name = os.path.basename(filename)
|
476
|
-
|
477
|
-
with tifffile.TiffFile(filename) as tif:
|
478
|
-
if (
|
479
|
-
not tif.is_ome
|
480
|
-
or len(tif.series) < 3
|
481
|
-
or tif.series[0].name != 'Phasor mean'
|
482
|
-
or tif.series[1].name != 'Phasor real'
|
483
|
-
or tif.series[2].name != 'Phasor imag'
|
484
|
-
):
|
485
|
-
raise ValueError(
|
486
|
-
f'{name!r} is not an OME-TIFF containing phasor images'
|
487
|
-
)
|
488
|
-
|
489
|
-
attrs: dict[str, Any] = {'dims': tuple(tif.series[0].axes)}
|
490
|
-
|
491
|
-
# TODO: read coords from OME-XML
|
492
|
-
ome_xml = tif.ome_metadata
|
493
|
-
assert ome_xml is not None
|
494
|
-
|
495
|
-
# TODO: parse OME-XML
|
496
|
-
match = re.search(
|
497
|
-
r'><Description>(.*)</Description><',
|
498
|
-
ome_xml,
|
499
|
-
re.MULTILINE | re.DOTALL,
|
500
|
-
)
|
501
|
-
if match is not None:
|
502
|
-
attrs['description'] = (
|
503
|
-
match.group(1)
|
504
|
-
.replace('&', '&')
|
505
|
-
.replace('>', '>')
|
506
|
-
.replace('<', '<')
|
507
|
-
)
|
508
|
-
|
509
|
-
has_harmonic_dim = tif.series[1].ndim > tif.series[0].ndim
|
510
|
-
nharmonics = tif.series[1].shape[0] if has_harmonic_dim else 1
|
511
|
-
harmonic_max = nharmonics
|
512
|
-
for i in (3, 4):
|
513
|
-
if len(tif.series) < i + 1:
|
514
|
-
break
|
515
|
-
series = tif.series[i]
|
516
|
-
data = series.asarray().squeeze()
|
517
|
-
if series.name == 'Phasor frequency':
|
518
|
-
attrs['frequency'] = float(data.item(0))
|
519
|
-
elif series.name == 'Phasor harmonic':
|
520
|
-
if not has_harmonic_dim and data.size == 1:
|
521
|
-
attrs['harmonic'] = int(data.item(0))
|
522
|
-
harmonic_max = attrs['harmonic']
|
523
|
-
elif has_harmonic_dim and data.size == nharmonics:
|
524
|
-
attrs['harmonic'] = data.tolist()
|
525
|
-
harmonic_max = max(attrs['harmonic'])
|
526
|
-
else:
|
527
|
-
logger.warning(
|
528
|
-
f'harmonic={data} does not match phasor '
|
529
|
-
f'shape={tif.series[1].shape}'
|
530
|
-
)
|
531
|
-
|
532
|
-
if 'harmonic' not in attrs:
|
533
|
-
if has_harmonic_dim:
|
534
|
-
attrs['harmonic'] = list(range(1, nharmonics + 1))
|
535
|
-
else:
|
536
|
-
attrs['harmonic'] = 1
|
537
|
-
harmonic_stored = attrs['harmonic']
|
538
|
-
|
539
|
-
mean = tif.series[0].asarray()
|
540
|
-
if harmonic is None:
|
541
|
-
# first harmonic in file
|
542
|
-
if isinstance(harmonic_stored, list):
|
543
|
-
attrs['harmonic'] = harmonic_stored[0]
|
544
|
-
else:
|
545
|
-
attrs['harmonic'] = harmonic_stored
|
546
|
-
real = tif.series[1].asarray()
|
547
|
-
if has_harmonic_dim:
|
548
|
-
real = real[0].copy()
|
549
|
-
imag = tif.series[2].asarray()
|
550
|
-
if has_harmonic_dim:
|
551
|
-
imag = imag[0].copy()
|
552
|
-
elif isinstance(harmonic, str) and harmonic == 'all':
|
553
|
-
# all harmonics as stored in file
|
554
|
-
real = tif.series[1].asarray()
|
555
|
-
imag = tif.series[2].asarray()
|
556
|
-
else:
|
557
|
-
# specified harmonics
|
558
|
-
harmonic, keepdims = parse_harmonic(harmonic, harmonic_max)
|
559
|
-
try:
|
560
|
-
if isinstance(harmonic_stored, list):
|
561
|
-
index = [harmonic_stored.index(h) for h in harmonic]
|
562
|
-
else:
|
563
|
-
index = [[harmonic_stored].index(h) for h in harmonic]
|
564
|
-
except ValueError as exc:
|
565
|
-
raise IndexError('harmonic not found') from exc
|
566
|
-
|
567
|
-
if has_harmonic_dim:
|
568
|
-
if keepdims:
|
569
|
-
attrs['harmonic'] = [harmonic_stored[i] for i in index]
|
570
|
-
real = tif.series[1].asarray()[index].copy()
|
571
|
-
imag = tif.series[2].asarray()[index].copy()
|
572
|
-
else:
|
573
|
-
attrs['harmonic'] = harmonic_stored[index[0]]
|
574
|
-
real = tif.series[1].asarray()[index[0]].copy()
|
575
|
-
imag = tif.series[2].asarray()[index[0]].copy()
|
576
|
-
elif keepdims:
|
577
|
-
real = tif.series[1].asarray()
|
578
|
-
real = real.reshape(1, *real.shape)
|
579
|
-
imag = tif.series[2].asarray()
|
580
|
-
imag = imag.reshape(1, *imag.shape)
|
581
|
-
attrs['harmonic'] = [harmonic_stored]
|
582
|
-
else:
|
583
|
-
real = tif.series[1].asarray()
|
584
|
-
imag = tif.series[2].asarray()
|
585
|
-
|
586
|
-
if real.shape != imag.shape:
|
587
|
-
logger.warning(f'{real.shape=} != {imag.shape=}')
|
588
|
-
if real.shape[-mean.ndim :] != mean.shape:
|
589
|
-
logger.warning(f'{real.shape[-mean.ndim:]=} != {mean.shape=}')
|
590
|
-
|
591
|
-
return mean, real, imag, attrs
|
592
|
-
|
593
|
-
|
594
|
-
def phasor_to_simfcs_referenced(
|
595
|
-
filename: str | PathLike[Any],
|
596
|
-
mean: ArrayLike,
|
597
|
-
real: ArrayLike,
|
598
|
-
imag: ArrayLike,
|
599
|
-
/,
|
600
|
-
*,
|
601
|
-
size: int | None = None,
|
602
|
-
dims: Sequence[str] | None = None,
|
603
|
-
) -> None:
|
604
|
-
"""Write phasor coordinate images to SimFCS referenced R64 file(s).
|
605
|
-
|
606
|
-
SimFCS referenced R64 files store square-shaped (commonly 256x256)
|
607
|
-
images of the average intensity, and the calibrated phasor coordinates
|
608
|
-
(encoded as phase and modulation) of two harmonics as ZIP-compressed,
|
609
|
-
single precision floating point arrays.
|
610
|
-
The file format does not support any metadata.
|
611
|
-
|
612
|
-
Images with more than two dimensions or larger than square size are
|
613
|
-
chunked to square-sized images and saved to separate files with
|
614
|
-
a name pattern, for example, "filename_T099_Y256_X000.r64".
|
615
|
-
Images or chunks with less than two dimensions or smaller than square size
|
616
|
-
are padded with NaN values.
|
617
|
-
|
618
|
-
Parameters
|
619
|
-
----------
|
620
|
-
filename : str or Path
|
621
|
-
Name of SimFCS referenced R64 file to write.
|
622
|
-
The file extension must be ``.r64``.
|
623
|
-
mean : array_like
|
624
|
-
Average intensity image.
|
625
|
-
real : array_like
|
626
|
-
Image of real component of calibrated phasor coordinates.
|
627
|
-
Multiple harmonics, if any, must be in the first dimension.
|
628
|
-
Harmonics must be starting at and increasing by one.
|
629
|
-
imag : array_like
|
630
|
-
Image of imaginary component of calibrated phasor coordinates.
|
631
|
-
Multiple harmonics, if any, must be in the first dimension.
|
632
|
-
Harmonics must be starting at and increasing by one.
|
633
|
-
size : int, optional
|
634
|
-
Size of X and Y dimensions of square-sized images stored in file.
|
635
|
-
By default, ``size = min(256, max(4, sizey, sizex))``.
|
636
|
-
dims : sequence of str, optional
|
637
|
-
Character codes for `mean` dimensions used to format file names.
|
638
|
-
|
639
|
-
See Also
|
640
|
-
--------
|
641
|
-
phasorpy.io.phasor_from_simfcs_referenced
|
642
|
-
|
643
|
-
Examples
|
644
|
-
--------
|
645
|
-
>>> mean, real, imag = numpy.random.rand(3, 32, 32)
|
646
|
-
>>> phasor_to_simfcs_referenced('_phasorpy.r64', mean, real, imag)
|
647
|
-
|
648
|
-
"""
|
649
|
-
filename, ext = os.path.splitext(filename)
|
650
|
-
if ext.lower() != '.r64':
|
651
|
-
raise ValueError(f'file extension {ext} != .r64')
|
652
|
-
|
653
|
-
# TODO: delay conversions to numpy arrays to inner loop
|
654
|
-
mean = numpy.asarray(mean, numpy.float32)
|
655
|
-
phi, mod = phasor_to_polar(real, imag, dtype=numpy.float32)
|
656
|
-
del real
|
657
|
-
del imag
|
658
|
-
phi = numpy.rad2deg(phi)
|
659
|
-
|
660
|
-
if phi.shape != mod.shape:
|
661
|
-
raise ValueError(f'{phi.shape=} != {mod.shape=}')
|
662
|
-
if mean.shape != phi.shape[-mean.ndim :]:
|
663
|
-
raise ValueError(f'{mean.shape=} != {phi.shape[-mean.ndim:]=}')
|
664
|
-
if phi.ndim == mean.ndim:
|
665
|
-
phi = phi.reshape(1, *phi.shape)
|
666
|
-
mod = mod.reshape(1, *mod.shape)
|
667
|
-
nharmonic = phi.shape[0]
|
668
|
-
|
669
|
-
if mean.ndim < 2:
|
670
|
-
# not an image
|
671
|
-
mean = mean.reshape(1, -1)
|
672
|
-
phi = phi.reshape(nharmonic, 1, -1)
|
673
|
-
mod = mod.reshape(nharmonic, 1, -1)
|
674
|
-
|
675
|
-
# TODO: investigate actual size and harmonics limits of SimFCS
|
676
|
-
sizey, sizex = mean.shape[-2:]
|
677
|
-
if size is None:
|
678
|
-
size = min(256, max(4, sizey, sizex))
|
679
|
-
elif not 4 <= size <= 65535:
|
680
|
-
raise ValueError(f'{size=} out of range [4, 65535]')
|
681
|
-
|
682
|
-
harmonics_per_file = 2 # TODO: make this a parameter?
|
683
|
-
chunk_shape = tuple(
|
684
|
-
[max(harmonics_per_file, 2)] + ([1] * (phi.ndim - 3)) + [size, size]
|
685
|
-
)
|
686
|
-
multi_file = any(i / j > 1 for i, j in zip(phi.shape, chunk_shape))
|
687
|
-
|
688
|
-
if dims is not None and len(dims) == phi.ndim - 1:
|
689
|
-
dims = tuple(dims)
|
690
|
-
dims = ('h' if dims[0].islower() else 'H',) + dims
|
691
|
-
|
692
|
-
chunk = numpy.empty((size, size), dtype=numpy.float32)
|
693
|
-
|
694
|
-
def rawdata_append(
|
695
|
-
rawdata: list[bytes], a: NDArray[Any] | None = None
|
696
|
-
) -> None:
|
697
|
-
if a is None:
|
698
|
-
chunk[:] = numpy.nan
|
699
|
-
rawdata.append(chunk.tobytes())
|
700
|
-
else:
|
701
|
-
sizey, sizex = a.shape[-2:]
|
702
|
-
if sizey == size and sizex == size:
|
703
|
-
rawdata.append(a.tobytes())
|
704
|
-
elif sizey <= size and sizex <= size:
|
705
|
-
chunk[:sizey, :sizex] = a[..., :sizey, :sizex]
|
706
|
-
chunk[:sizey, sizex:] = numpy.nan
|
707
|
-
chunk[sizey:, :] = numpy.nan
|
708
|
-
rawdata.append(chunk.tobytes())
|
709
|
-
else:
|
710
|
-
raise RuntimeError # should not be reached
|
711
|
-
|
712
|
-
for index, label, _ in chunk_iter(
|
713
|
-
phi.shape, chunk_shape, dims, squeeze=False, use_index=True
|
714
|
-
):
|
715
|
-
rawdata = [struct.pack('I', size)]
|
716
|
-
rawdata_append(rawdata, mean[index[1:]])
|
717
|
-
phi_ = phi[index]
|
718
|
-
mod_ = mod[index]
|
719
|
-
for i in range(phi_.shape[0]):
|
720
|
-
rawdata_append(rawdata, phi_[i])
|
721
|
-
rawdata_append(rawdata, mod_[i])
|
722
|
-
if phi_.shape[0] == 1:
|
723
|
-
rawdata_append(rawdata)
|
724
|
-
rawdata_append(rawdata)
|
725
|
-
|
726
|
-
if not multi_file:
|
727
|
-
label = ''
|
728
|
-
with open(filename + label + ext, 'wb') as fh:
|
729
|
-
fh.write(zlib.compress(b''.join(rawdata)))
|
730
|
-
|
731
|
-
|
732
|
-
def phasor_from_simfcs_referenced(
|
733
|
-
filename: str | PathLike[Any],
|
734
|
-
/,
|
735
|
-
*,
|
736
|
-
harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
|
737
|
-
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], dict[str, Any]]:
|
738
|
-
"""Return phasor coordinates and metadata from SimFCS REF or R64 file.
|
739
|
-
|
740
|
-
SimFCS referenced REF and R64 files contain phasor coordinate images
|
741
|
-
(encoded as phase and modulation) for two harmonics.
|
742
|
-
Phasor coordinates from lifetime-resolved signals are calibrated.
|
743
|
-
|
744
|
-
Parameters
|
745
|
-
----------
|
746
|
-
filename : str or Path
|
747
|
-
Name of SimFCS REF or R64 file to read.
|
748
|
-
harmonic : int or sequence of int, optional
|
749
|
-
Harmonic(s) to include in returned phasor coordinates.
|
750
|
-
By default, only the first harmonic is returned.
|
751
|
-
|
752
|
-
Returns
|
753
|
-
-------
|
754
|
-
mean : ndarray
|
755
|
-
Average intensity image.
|
756
|
-
real : ndarray
|
757
|
-
Image of real component of phasor coordinates.
|
758
|
-
Multiple harmonics, if any, are in the first axis.
|
759
|
-
imag : ndarray
|
760
|
-
Image of imaginary component of phasor coordinates.
|
761
|
-
Multiple harmonics, if any, are in the first axis.
|
762
|
-
attrs : dict
|
763
|
-
Select metadata:
|
764
|
-
|
765
|
-
- ``'dims'`` (tuple of str):
|
766
|
-
:ref:`Axes codes <axes>` for `mean` image dimensions.
|
767
|
-
|
768
|
-
Raises
|
769
|
-
------
|
770
|
-
lfdfiles.LfdfileError
|
771
|
-
File is not a SimFCS REF or R64 file.
|
772
|
-
|
773
|
-
See Also
|
774
|
-
--------
|
775
|
-
phasorpy.io.phasor_to_simfcs_referenced
|
776
|
-
|
777
|
-
Notes
|
778
|
-
-----
|
779
|
-
The implementation is based on the
|
780
|
-
`lfdfiles <https://github.com/cgohlke/lfdfiles/>`__ library.
|
781
|
-
|
782
|
-
Examples
|
783
|
-
--------
|
784
|
-
>>> phasor_to_simfcs_referenced(
|
785
|
-
... '_phasorpy.r64', *numpy.random.rand(3, 32, 32)
|
786
|
-
... )
|
787
|
-
>>> mean, real, imag, _ = phasor_from_simfcs_referenced('_phasorpy.r64')
|
788
|
-
>>> mean
|
789
|
-
array([[...]], dtype=float32)
|
790
|
-
|
791
|
-
"""
|
792
|
-
import lfdfiles
|
793
|
-
|
794
|
-
ext = os.path.splitext(filename)[-1].lower()
|
795
|
-
if ext == '.r64':
|
796
|
-
with lfdfiles.SimfcsR64(filename) as r64:
|
797
|
-
data = r64.asarray()
|
798
|
-
elif ext == '.ref':
|
799
|
-
with lfdfiles.SimfcsRef(filename) as ref:
|
800
|
-
data = ref.asarray()
|
801
|
-
else:
|
802
|
-
raise ValueError(f'file extension must be .ref or .r64, not {ext!r}')
|
803
|
-
|
804
|
-
harmonic, keep_harmonic_dim = parse_harmonic(harmonic, data.shape[0] // 2)
|
805
|
-
|
806
|
-
mean = data[0].copy()
|
807
|
-
real = numpy.empty((len(harmonic),) + mean.shape, numpy.float32)
|
808
|
-
imag = numpy.empty_like(real)
|
809
|
-
for i, h in enumerate(harmonic):
|
810
|
-
h = (h - 1) * 2 + 1
|
811
|
-
re, im = phasor_from_polar(numpy.deg2rad(data[h]), data[h + 1])
|
812
|
-
real[i] = re
|
813
|
-
imag[i] = im
|
814
|
-
if not keep_harmonic_dim:
|
815
|
-
real = real.reshape(mean.shape)
|
816
|
-
imag = imag.reshape(mean.shape)
|
817
|
-
|
818
|
-
return mean, real, imag, {'dims': ('Y', 'X')}
|
819
|
-
|
820
|
-
|
821
|
-
def phasor_from_ifli(
|
822
|
-
filename: str | PathLike[Any],
|
823
|
-
/,
|
824
|
-
*,
|
825
|
-
channel: int | None = None,
|
826
|
-
harmonic: int | Sequence[int] | Literal['all', 'any'] | str | None = None,
|
827
|
-
**kwargs: Any,
|
828
|
-
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], dict[str, Any]]:
|
829
|
-
"""Return phasor coordinates and metadata from ISS IFLI file.
|
830
|
-
|
831
|
-
ISS VistaVision IFLI files contain calibrated phasor coordinates for
|
832
|
-
possibly several positions, wavelengths, time points, channels, slices,
|
833
|
-
and frequencies from analog or digital frequency-domain fluorescence
|
834
|
-
lifetime measurements.
|
835
|
-
|
836
|
-
Parameters
|
837
|
-
----------
|
838
|
-
filename : str or Path
|
839
|
-
Name of ISS IFLI file to read.
|
840
|
-
channel : int, optional
|
841
|
-
If None (default), return all channels, else return specified channel.
|
842
|
-
harmonic : int, sequence of int, or 'all', optional
|
843
|
-
Harmonic(s) to return from file.
|
844
|
-
If None (default), return the first harmonic stored in file.
|
845
|
-
If `'all'`, return all harmonics of first frequency stored in file.
|
846
|
-
If `'any'`, return all frequencies as stored in file, not necessarily
|
847
|
-
harmonics of the first frequency.
|
848
|
-
If a list, the first axes of the returned `real` and `imag` arrays
|
849
|
-
contain specified harmonic(s).
|
850
|
-
If an integer, the returned `real` and `imag` arrays are single
|
851
|
-
harmonic and have the same shape as `mean`.
|
852
|
-
**kwargs
|
853
|
-
Additional arguments passed to :py:meth:`lfdfiles.VistaIfli.asarray`,
|
854
|
-
for example ``memmap=True``.
|
855
|
-
|
856
|
-
Returns
|
857
|
-
-------
|
858
|
-
mean : ndarray
|
859
|
-
Average intensity image.
|
860
|
-
May have up to 7 dimensions in ``'RETCZYX'`` order.
|
861
|
-
real : ndarray
|
862
|
-
Image of real component of phasor coordinates.
|
863
|
-
Same shape as `mean`, except it may have a harmonic/frequency
|
864
|
-
dimension prepended.
|
865
|
-
imag : ndarray
|
866
|
-
Image of imaginary component of phasor coordinates.
|
867
|
-
Same shape as `real`.
|
868
|
-
attrs : dict
|
869
|
-
Select metadata:
|
870
|
-
|
871
|
-
- ``'dims'`` (tuple of str):
|
872
|
-
:ref:`Axes codes <axes>` for `mean` image dimensions.
|
873
|
-
- ``'harmonic'`` (int or list of int):
|
874
|
-
Harmonic(s) present in `real` and `imag`.
|
875
|
-
If a scalar, `real` and `imag` are single harmonic and contain no
|
876
|
-
harmonic axes.
|
877
|
-
If a list, `real` and `imag` contain one or more harmonics in the
|
878
|
-
first axis.
|
879
|
-
- ``'frequency'`` (float):
|
880
|
-
Fundamental frequency of time-resolved phasor coordinates in MHz.
|
881
|
-
- ``'samples'`` (int):
|
882
|
-
Number of samples per frequency.
|
883
|
-
- ``'ifli_header'`` (dict):
|
884
|
-
Metadata from IFLI file header.
|
885
|
-
|
886
|
-
Raises
|
887
|
-
------
|
888
|
-
lfdfiles.LfdFileError
|
889
|
-
File is not an ISS IFLI file.
|
890
|
-
IndexError
|
891
|
-
Harmonic is not found in file.
|
892
|
-
|
893
|
-
Notes
|
894
|
-
-----
|
895
|
-
The implementation is based on the
|
896
|
-
`lfdfiles <https://github.com/cgohlke/lfdfiles/>`__ library.
|
897
|
-
|
898
|
-
Examples
|
899
|
-
--------
|
900
|
-
>>> mean, real, imag, attr = phasor_from_ifli(
|
901
|
-
... fetch('frequency_domain.ifli'), harmonic='all'
|
902
|
-
... )
|
903
|
-
>>> mean.shape
|
904
|
-
(256, 256)
|
905
|
-
>>> real.shape
|
906
|
-
(4, 256, 256)
|
907
|
-
>>> attr['dims']
|
908
|
-
('Y', 'X')
|
909
|
-
>>> attr['harmonic']
|
910
|
-
[1, 2, 3, 5]
|
911
|
-
>>> attr['frequency'] # doctest: +NUMBER
|
912
|
-
80.33
|
913
|
-
>>> attr['samples']
|
914
|
-
64
|
915
|
-
>>> attr['ifli_header']
|
916
|
-
{'Version': 16, ... 'ModFrequency': (...), 'RefLifetime': (2.5,), ...}
|
917
|
-
|
918
|
-
"""
|
919
|
-
import lfdfiles
|
920
|
-
|
921
|
-
with lfdfiles.VistaIfli(filename) as ifli:
|
922
|
-
assert ifli.axes is not None
|
923
|
-
data = ifli.asarray(**kwargs)
|
924
|
-
header = ifli.header
|
925
|
-
axes = ifli.axes
|
926
|
-
|
927
|
-
if channel is not None:
|
928
|
-
data = data[:, :, :, channel]
|
929
|
-
axes = axes[:3] + axes[4:]
|
930
|
-
|
931
|
-
shape, dims, _ = _squeeze_dims(data.shape, axes, skip='YXF')
|
932
|
-
data = data.reshape(shape)
|
933
|
-
data = numpy.moveaxis(data, -2, 0) # move frequency to first axis
|
934
|
-
mean = data[..., 0].mean(axis=0) # average frequencies
|
935
|
-
real = data[..., 1].copy()
|
936
|
-
imag = data[..., 2].copy()
|
937
|
-
dims = dims[:-2]
|
938
|
-
del data
|
939
|
-
|
940
|
-
samples = header['HistogramResolution']
|
941
|
-
frequencies = header['ModFrequency']
|
942
|
-
frequency = frequencies[0]
|
943
|
-
harmonic_stored = [
|
944
|
-
(
|
945
|
-
int(round(f / frequency))
|
946
|
-
if (0.99 < f / frequency % 1.0) < 1.01
|
947
|
-
else None
|
948
|
-
)
|
949
|
-
for f in frequencies
|
950
|
-
]
|
951
|
-
|
952
|
-
index: int | list[int]
|
953
|
-
if harmonic is None:
|
954
|
-
# return first harmonic in file
|
955
|
-
keepdims = False
|
956
|
-
harmonic = [1]
|
957
|
-
index = [0]
|
958
|
-
elif isinstance(harmonic, str) and harmonic in {'all', 'any'}:
|
959
|
-
keepdims = True
|
960
|
-
if harmonic == 'any':
|
961
|
-
# return any frequency
|
962
|
-
harmonic = [
|
963
|
-
(frequencies[i] / frequency if h is None else h)
|
964
|
-
for i, h in enumerate(harmonic_stored)
|
965
|
-
]
|
966
|
-
index = list(range(len(harmonic_stored)))
|
967
|
-
else:
|
968
|
-
# return only harmonics of first frequency
|
969
|
-
harmonic = [h for h in harmonic_stored if h is not None]
|
970
|
-
index = [i for i, h in enumerate(harmonic_stored) if h is not None]
|
971
|
-
else:
|
972
|
-
# return specified harmonics
|
973
|
-
harmonic, keepdims = parse_harmonic(
|
974
|
-
harmonic, max(h for h in harmonic_stored if h is not None)
|
975
|
-
)
|
976
|
-
try:
|
977
|
-
index = [harmonic_stored.index(h) for h in harmonic]
|
978
|
-
except ValueError as exc:
|
979
|
-
raise IndexError('harmonic not found') from exc
|
980
|
-
|
981
|
-
real = real[index]
|
982
|
-
imag = imag[index]
|
983
|
-
if not keepdims:
|
984
|
-
real = real[0]
|
985
|
-
imag = imag[0]
|
986
|
-
|
987
|
-
attrs = {
|
988
|
-
'dims': tuple(dims),
|
989
|
-
'harmonic': harmonic,
|
990
|
-
'frequency': frequency * 1e-6,
|
991
|
-
'samples': samples,
|
992
|
-
'ifli_header': header,
|
993
|
-
}
|
994
|
-
|
995
|
-
return mean, real, imag, attrs
|
996
|
-
|
997
|
-
|
998
|
-
def phasor_from_lif(
|
999
|
-
filename: str | PathLike[Any],
|
1000
|
-
/,
|
1001
|
-
image: str | None = None,
|
1002
|
-
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], dict[str, Any]]:
|
1003
|
-
"""Return phasor coordinates and metadata from Leica image file.
|
1004
|
-
|
1005
|
-
Leica image files may contain uncalibrated phasor coordinate images and
|
1006
|
-
metadata from the analysis of FLIM measurements.
|
1007
|
-
|
1008
|
-
Parameters
|
1009
|
-
----------
|
1010
|
-
filename : str or Path
|
1011
|
-
Name of Leica image file to read.
|
1012
|
-
image : str, optional
|
1013
|
-
Name of parent image containing phasor coordinates.
|
1014
|
-
|
1015
|
-
Returns
|
1016
|
-
-------
|
1017
|
-
mean : ndarray
|
1018
|
-
Average intensity image.
|
1019
|
-
real : ndarray
|
1020
|
-
Image of real component of phasor coordinates.
|
1021
|
-
imag : ndarray
|
1022
|
-
Image of imaginary component of phasor coordinates.
|
1023
|
-
attrs : dict
|
1024
|
-
Select metadata:
|
1025
|
-
|
1026
|
-
- ``'dims'`` (tuple of str):
|
1027
|
-
:ref:`Axes codes <axes>` for `mean` image dimensions.
|
1028
|
-
- ``'frequency'`` (float):
|
1029
|
-
Fundamental frequency of time-resolved phasor coordinates in MHz.
|
1030
|
-
May not be present in all files.
|
1031
|
-
- ``'flim_rawdata'`` (dict):
|
1032
|
-
Settings from SingleMoleculeDetection/RawData XML element.
|
1033
|
-
- ``'flim_phasor_channels'`` (list of dict):
|
1034
|
-
Settings from SingleMoleculeDetection/.../PhasorData/Channels XML
|
1035
|
-
elements.
|
1036
|
-
|
1037
|
-
Raises
|
1038
|
-
------
|
1039
|
-
liffile.LifFileError
|
1040
|
-
File is not a Leica image file.
|
1041
|
-
ValueError
|
1042
|
-
File or `image` do not contain phasor coordinates and metadata.
|
1043
|
-
|
1044
|
-
Notes
|
1045
|
-
-----
|
1046
|
-
The implementation is based on the
|
1047
|
-
`liffile <https://github.com/cgohlke/liffile/>`__ library.
|
1048
|
-
|
1049
|
-
Examples
|
1050
|
-
--------
|
1051
|
-
>>> mean, real, imag, attrs = phasor_from_lif(fetch('FLIM_testdata.lif'))
|
1052
|
-
>>> real.shape
|
1053
|
-
(1024, 1024)
|
1054
|
-
>>> attrs['dims']
|
1055
|
-
('Y', 'X')
|
1056
|
-
>>> attrs['frequency']
|
1057
|
-
19.505
|
1058
|
-
|
1059
|
-
"""
|
1060
|
-
# TODO: read harmonic from XML if possible
|
1061
|
-
# TODO: get calibration settings from XML metadata, lifetime, or
|
1062
|
-
# phasor plot images
|
1063
|
-
import liffile
|
1064
|
-
|
1065
|
-
image = '' if image is None else f'.*{image}.*/'
|
1066
|
-
samples = 1
|
1067
|
-
|
1068
|
-
with liffile.LifFile(filename) as lif:
|
1069
|
-
try:
|
1070
|
-
im = lif.images[image + 'Phasor Intensity$']
|
1071
|
-
dims = im.dims
|
1072
|
-
coords = im.coords
|
1073
|
-
# meta = image.attrs
|
1074
|
-
mean = im.asarray().astype(numpy.float32)
|
1075
|
-
real = lif.images[image + 'Phasor Real$'].asarray()
|
1076
|
-
imag = lif.images[image + 'Phasor Imaginary$'].asarray()
|
1077
|
-
# mask = lif.images[image + 'Phasor Mask$'].asarray()
|
1078
|
-
except Exception as exc:
|
1079
|
-
raise ValueError(
|
1080
|
-
f'{lif.filename!r} does not contain Phasor images'
|
1081
|
-
) from exc
|
1082
|
-
|
1083
|
-
attrs: dict[str, Any] = {'dims': dims, 'coords': coords}
|
1084
|
-
flim = im.parent_image
|
1085
|
-
if flim is not None and isinstance(flim, liffile.LifFlimImage):
|
1086
|
-
xml = flim.parent.xml_element
|
1087
|
-
frequency = xml.find('.//Dataset/RawData/LaserPulseFrequency')
|
1088
|
-
if frequency is not None and frequency.text is not None:
|
1089
|
-
attrs['frequency'] = float(frequency.text) * 1e-6
|
1090
|
-
clock_period = xml.find('.//Dataset/RawData/ClockPeriod')
|
1091
|
-
if clock_period is not None and clock_period.text is not None:
|
1092
|
-
tmp = float(clock_period.text) * float(frequency.text)
|
1093
|
-
samples = int(round(1.0 / tmp))
|
1094
|
-
attrs['samples'] = samples
|
1095
|
-
channels = []
|
1096
|
-
for channel in xml.findall(
|
1097
|
-
'.//Dataset/FlimData/PhasorData/Channels'
|
1098
|
-
):
|
1099
|
-
ch = liffile.xml2dict(channel)['Channels']
|
1100
|
-
ch.pop('PhasorPlotShapes', None)
|
1101
|
-
channels.append(ch)
|
1102
|
-
attrs['flim_phasor_channels'] = channels
|
1103
|
-
attrs['flim_rawdata'] = flim.attrs.get('RawData', {})
|
1104
|
-
|
1105
|
-
if samples > 1:
|
1106
|
-
mean /= samples
|
1107
|
-
return (
|
1108
|
-
mean,
|
1109
|
-
real.astype(numpy.float32),
|
1110
|
-
imag.astype(numpy.float32),
|
1111
|
-
attrs,
|
1112
|
-
)
|
1113
|
-
|
1114
|
-
|
1115
|
-
def lifetime_from_lif(
|
1116
|
-
filename: str | PathLike[Any],
|
1117
|
-
/,
|
1118
|
-
image: str | None = None,
|
1119
|
-
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], dict[str, Any]]:
|
1120
|
-
"""Return lifetime image and metadata from Leica image file.
|
1121
|
-
|
1122
|
-
Leica image files may contain fluorescence lifetime images and metadata
|
1123
|
-
from the analysis of FLIM measurements.
|
1124
|
-
|
1125
|
-
Parameters
|
1126
|
-
----------
|
1127
|
-
filename : str or Path
|
1128
|
-
Name of Leica image file to read.
|
1129
|
-
image : str, optional
|
1130
|
-
Name of parent image containing lifetime image.
|
1131
|
-
|
1132
|
-
Returns
|
1133
|
-
-------
|
1134
|
-
lifetime : ndarray
|
1135
|
-
Fluorescence lifetime image in ns.
|
1136
|
-
intensity : ndarray
|
1137
|
-
Fluorescence intensity image.
|
1138
|
-
stddev : ndarray
|
1139
|
-
Standard deviation of fluorescence lifetimes in ns.
|
1140
|
-
attrs : dict
|
1141
|
-
Select metadata:
|
1142
|
-
|
1143
|
-
- ``'dims'`` (tuple of str):
|
1144
|
-
:ref:`Axes codes <axes>` for `intensity` image dimensions.
|
1145
|
-
- ``'frequency'`` (float):
|
1146
|
-
Fundamental frequency of lifetimes in MHz.
|
1147
|
-
May not be present in all files.
|
1148
|
-
- ``'samples'`` (int):
|
1149
|
-
Number of bins in TCSPC histogram. May not be present in all files.
|
1150
|
-
- ``'flim_rawdata'`` (dict):
|
1151
|
-
Settings from SingleMoleculeDetection/RawData XML element.
|
1152
|
-
|
1153
|
-
Raises
|
1154
|
-
------
|
1155
|
-
liffile.LifFileError
|
1156
|
-
File is not a Leica image file.
|
1157
|
-
ValueError
|
1158
|
-
File or `image` does not contain lifetime coordinates and metadata.
|
1159
|
-
|
1160
|
-
Notes
|
1161
|
-
-----
|
1162
|
-
The implementation is based on the
|
1163
|
-
`liffile <https://github.com/cgohlke/liffile/>`__ library.
|
1164
|
-
|
1165
|
-
Examples
|
1166
|
-
--------
|
1167
|
-
>>> lifetime, intensity, stddev, attrs = lifetime_from_lif(
|
1168
|
-
... fetch('FLIM_testdata.lif')
|
1169
|
-
... )
|
1170
|
-
>>> lifetime.shape
|
1171
|
-
(1024, 1024)
|
1172
|
-
>>> attrs['dims']
|
1173
|
-
('Y', 'X')
|
1174
|
-
>>> attrs['frequency']
|
1175
|
-
19.505
|
1176
|
-
|
1177
|
-
"""
|
1178
|
-
import liffile
|
1179
|
-
|
1180
|
-
image = '' if image is None else f'.*{image}.*/'
|
1181
|
-
|
1182
|
-
with liffile.LifFile(filename) as lif:
|
1183
|
-
try:
|
1184
|
-
im = lif.images[image + 'Intensity$']
|
1185
|
-
dims = im.dims
|
1186
|
-
coords = im.coords
|
1187
|
-
# meta = im.attrs
|
1188
|
-
intensity = im.asarray()
|
1189
|
-
lifetime = lif.images[image + 'Fast Flim$'].asarray()
|
1190
|
-
stddev = lif.images[image + 'Standard Deviation$'].asarray()
|
1191
|
-
except Exception as exc:
|
1192
|
-
raise ValueError(
|
1193
|
-
f'{lif.filename!r} does not contain lifetime images'
|
1194
|
-
) from exc
|
1195
|
-
|
1196
|
-
attrs: dict[str, Any] = {'dims': dims, 'coords': coords}
|
1197
|
-
flim = im.parent_image
|
1198
|
-
if flim is not None and isinstance(flim, liffile.LifFlimImage):
|
1199
|
-
xml = flim.parent.xml_element
|
1200
|
-
frequency = xml.find('.//Dataset/RawData/LaserPulseFrequency')
|
1201
|
-
if frequency is not None and frequency.text is not None:
|
1202
|
-
attrs['frequency'] = float(frequency.text) * 1e-6
|
1203
|
-
clock_period = xml.find('.//Dataset/RawData/ClockPeriod')
|
1204
|
-
if clock_period is not None and clock_period.text is not None:
|
1205
|
-
tmp = float(clock_period.text) * float(frequency.text)
|
1206
|
-
samples = int(round(1.0 / tmp))
|
1207
|
-
attrs['samples'] = samples
|
1208
|
-
attrs['flim_rawdata'] = flim.attrs.get('RawData', {})
|
1209
|
-
|
1210
|
-
return (
|
1211
|
-
lifetime.astype(numpy.float32),
|
1212
|
-
intensity.astype(numpy.float32),
|
1213
|
-
stddev.astype(numpy.float32),
|
1214
|
-
attrs,
|
1215
|
-
)
|
1216
|
-
|
1217
|
-
|
1218
|
-
def phasor_from_flimlabs_json(
|
1219
|
-
filename: str | PathLike[Any],
|
1220
|
-
/,
|
1221
|
-
channel: int | None = None,
|
1222
|
-
harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
|
1223
|
-
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], dict[str, Any]]:
|
1224
|
-
"""Return phasor coordinates and metadata from FLIM LABS JSON phasor file.
|
1225
|
-
|
1226
|
-
FLIM LABS JSON files may contain calibrated phasor coordinates
|
1227
|
-
(possibly for multiple channels and harmonics) and metadata from
|
1228
|
-
digital frequency-domain measurements.
|
1229
|
-
|
1230
|
-
Parameters
|
1231
|
-
----------
|
1232
|
-
filename : str or Path
|
1233
|
-
Name of FLIM LABS JSON phasor file to read.
|
1234
|
-
The file name usually contains the string "_phasor".
|
1235
|
-
channel : int, optional
|
1236
|
-
If None (default), return all channels, else return specified channel.
|
1237
|
-
harmonic : int, sequence of int, or 'all', optional
|
1238
|
-
Harmonic(s) to return from file.
|
1239
|
-
If None (default), return the first harmonic stored in the file.
|
1240
|
-
If `'all'`, return all harmonics as stored in file.
|
1241
|
-
If a list, the first axes of the returned `real` and `imag` arrays
|
1242
|
-
contain specified harmonic(s).
|
1243
|
-
If an integer, the returned `real` and `imag` arrays are single
|
1244
|
-
harmonic and have the same shape as `mean`.
|
1245
|
-
|
1246
|
-
Returns
|
1247
|
-
-------
|
1248
|
-
mean : ndarray
|
1249
|
-
Average intensity image.
|
1250
|
-
Zeroed if an intensity image is not present in file.
|
1251
|
-
real : ndarray
|
1252
|
-
Image of real component of phasor coordinates.
|
1253
|
-
imag : ndarray
|
1254
|
-
Image of imaginary component of phasor coordinates.
|
1255
|
-
attrs : dict
|
1256
|
-
Select metadata:
|
1257
|
-
|
1258
|
-
- ``'dims'`` (tuple of str):
|
1259
|
-
:ref:`Axes codes <axes>` for `mean` image dimensions.
|
1260
|
-
- ``'harmonic'`` (int):
|
1261
|
-
Harmonic of `real` and `imag`.
|
1262
|
-
- ``'frequency'`` (float):
|
1263
|
-
Fundamental frequency of time-resolved phasor coordinates in MHz.
|
1264
|
-
- ``'flimlabs_header'`` (dict):
|
1265
|
-
FLIM LABS file header.
|
1266
|
-
|
1267
|
-
Raises
|
1268
|
-
------
|
1269
|
-
ValueError
|
1270
|
-
File is not a FLIM LABS JSON file containing phasor coordinates.
|
1271
|
-
IndexError
|
1272
|
-
Harmonic or channel not found in file.
|
1273
|
-
|
1274
|
-
See Also
|
1275
|
-
--------
|
1276
|
-
phasorpy.io.signal_from_flimlabs_json
|
1277
|
-
|
1278
|
-
Examples
|
1279
|
-
--------
|
1280
|
-
>>> mean, real, imag, attrs = phasor_from_flimlabs_json(
|
1281
|
-
... fetch('Convallaria_m2_1740751781_phasor_ch1.json'), harmonic='all'
|
1282
|
-
... )
|
1283
|
-
>>> real.shape
|
1284
|
-
(3, 256, 256)
|
1285
|
-
>>> attrs['dims']
|
1286
|
-
('Y', 'X')
|
1287
|
-
>>> attrs['harmonic']
|
1288
|
-
[1, 2, 3]
|
1289
|
-
>>> attrs['frequency'] # doctest: +NUMBER
|
1290
|
-
40.00
|
1291
|
-
|
1292
|
-
"""
|
1293
|
-
import json
|
1294
|
-
|
1295
|
-
with open(filename, 'rb') as fh:
|
1296
|
-
try:
|
1297
|
-
data = json.load(fh)
|
1298
|
-
except Exception as exc:
|
1299
|
-
raise ValueError('not a valid JSON file') from exc
|
1300
|
-
|
1301
|
-
if (
|
1302
|
-
'header' not in data
|
1303
|
-
or 'phasors_data' not in data
|
1304
|
-
or 'laser_period_ns' not in data['header']
|
1305
|
-
or 'file_id' not in data['header']
|
1306
|
-
# or data['header']['file_id'] != [73, 80, 71, 49] # 'IPG1'
|
1307
|
-
):
|
1308
|
-
raise ValueError(
|
1309
|
-
'not a FLIM LABS JSON file containing phasor coordinates'
|
1310
|
-
)
|
1311
|
-
|
1312
|
-
header = data['header']
|
1313
|
-
phasor_data = data['phasors_data']
|
1314
|
-
|
1315
|
-
harmonics = []
|
1316
|
-
channels = [] # 1-based
|
1317
|
-
for d in phasor_data:
|
1318
|
-
h = d['harmonic']
|
1319
|
-
if h not in harmonics:
|
1320
|
-
harmonics.append(h)
|
1321
|
-
c = d['channel']
|
1322
|
-
if c not in channels:
|
1323
|
-
channels.append(c)
|
1324
|
-
harmonics = sorted(harmonics)
|
1325
|
-
channels = sorted(channels)
|
1326
|
-
|
1327
|
-
if channel is not None:
|
1328
|
-
if channel + 1 not in channels:
|
1329
|
-
raise IndexError(f'{channel=}')
|
1330
|
-
channel += 1 # 1-based index
|
1331
|
-
|
1332
|
-
if isinstance(harmonic, str) and harmonic == 'all':
|
1333
|
-
harmonic = harmonics
|
1334
|
-
keep_harmonic_axis = True
|
1335
|
-
else:
|
1336
|
-
harmonic, keep_harmonic_axis = parse_harmonic(harmonic, harmonics[-1])
|
1337
|
-
if any(h not in harmonics for h in harmonic):
|
1338
|
-
raise IndexError(f'{harmonic=} not in {harmonics!r}')
|
1339
|
-
harmonic_index = {h: i for i, h in enumerate(harmonic)}
|
1340
|
-
|
1341
|
-
nharmonics = len(harmonic)
|
1342
|
-
nchannels = len(channels) if channel is None else 1
|
1343
|
-
height = header['image_height']
|
1344
|
-
width = header['image_width']
|
1345
|
-
dtype = numpy.float32
|
1346
|
-
|
1347
|
-
shape: tuple[int, ...] = nharmonics, nchannels, height, width
|
1348
|
-
axes: str = 'CYX'
|
1349
|
-
mean = numpy.zeros(shape[1:], dtype)
|
1350
|
-
real = numpy.zeros(shape, dtype)
|
1351
|
-
imag = numpy.zeros(shape, dtype)
|
1352
|
-
|
1353
|
-
for d in phasor_data:
|
1354
|
-
h = d['harmonic']
|
1355
|
-
if h not in harmonic_index:
|
1356
|
-
continue
|
1357
|
-
h = harmonic_index[h]
|
1358
|
-
if channel is not None:
|
1359
|
-
if d['channel'] != channel:
|
1360
|
-
continue
|
1361
|
-
c = 0
|
1362
|
-
else:
|
1363
|
-
c = channels.index(d['channel'])
|
1364
|
-
|
1365
|
-
real[h, c] = numpy.asarray(d['g_data'], dtype)
|
1366
|
-
imag[h, c] = numpy.asarray(d['s_data'], dtype)
|
1367
|
-
|
1368
|
-
if 'intensities_data' in data:
|
1369
|
-
from ._phasorpy import _flimlabs_mean
|
1370
|
-
|
1371
|
-
mean.shape = nchannels, height * width
|
1372
|
-
_flimlabs_mean(
|
1373
|
-
mean,
|
1374
|
-
data['intensities_data'],
|
1375
|
-
-1 if channel is None else channels.index(channel),
|
1376
|
-
)
|
1377
|
-
mean.shape = shape[1:]
|
1378
|
-
# JSON cannot store NaN values
|
1379
|
-
nan_mask = mean == 0
|
1380
|
-
real[:, nan_mask] = numpy.nan
|
1381
|
-
imag[:, nan_mask] = numpy.nan
|
1382
|
-
del nan_mask
|
1383
|
-
|
1384
|
-
if nchannels == 1:
|
1385
|
-
axes = axes[1:]
|
1386
|
-
mean = mean[0]
|
1387
|
-
real = real[:, 0]
|
1388
|
-
imag = imag[:, 0]
|
1389
|
-
|
1390
|
-
if not keep_harmonic_axis:
|
1391
|
-
real = real[0]
|
1392
|
-
imag = imag[0]
|
1393
|
-
|
1394
|
-
attrs = {
|
1395
|
-
'dims': tuple(axes),
|
1396
|
-
'samples': 256,
|
1397
|
-
'harmonic': harmonic if keep_harmonic_axis else harmonic[0],
|
1398
|
-
'frequency': 1000.0 / header['laser_period_ns'],
|
1399
|
-
'flimlabs_header': header,
|
1400
|
-
}
|
1401
|
-
|
1402
|
-
return mean, real, imag, attrs
|
1403
|
-
|
1404
|
-
|
1405
|
-
def signal_from_flimlabs_json(
|
1406
|
-
filename: str | PathLike[Any],
|
1407
|
-
/,
|
1408
|
-
*,
|
1409
|
-
channel: int | None = None,
|
1410
|
-
dtype: DTypeLike | None = None,
|
1411
|
-
) -> DataArray:
|
1412
|
-
"""Return TCSPC histogram and metadata from FLIM LABS JSON imaging file.
|
1413
|
-
|
1414
|
-
FLIM LABS JSON imaging files contain encoded, multi-channel TCSPC
|
1415
|
-
histograms and metadata from digital frequency-domain measurements.
|
1416
|
-
|
1417
|
-
Parameters
|
1418
|
-
----------
|
1419
|
-
filename : str or Path
|
1420
|
-
Name of FLIM LABS JSON imaging file to read.
|
1421
|
-
The file name usually contains the string "_imaging" or "_phasor".
|
1422
|
-
channel : int, optional
|
1423
|
-
If None (default), return all channels, else return specified channel.
|
1424
|
-
dtype : dtype-like, optional, default: uint16
|
1425
|
-
Unsigned integer type of TCSPC histogram.
|
1426
|
-
Increase the bit-depth for high photon counts.
|
1427
|
-
|
1428
|
-
Returns
|
1429
|
-
-------
|
1430
|
-
xarray.DataArray
|
1431
|
-
TCSPC histogram with :ref:`axes codes <axes>` ``'CYXH'`` and
|
1432
|
-
type specified in ``dtype``:
|
1433
|
-
|
1434
|
-
- ``coords['H']``: delay-times of histogram bins in ns.
|
1435
|
-
- ``attrs['frequency']``: laser repetition frequency in MHz.
|
1436
|
-
- ``attrs['flimlabs_header']``: FLIM LABS file header.
|
1437
|
-
|
1438
|
-
Raises
|
1439
|
-
------
|
1440
|
-
ValueError
|
1441
|
-
File is not a FLIM LABS JSON file containing TCSPC histogram.
|
1442
|
-
`dtype` is not an unsigned integer.
|
1443
|
-
IndexError
|
1444
|
-
Channel out of range.
|
1445
|
-
|
1446
|
-
See Also
|
1447
|
-
--------
|
1448
|
-
phasorpy.io.phasor_from_flimlabs_json
|
1449
|
-
|
1450
|
-
Examples
|
1451
|
-
--------
|
1452
|
-
>>> signal = signal_from_flimlabs_json(
|
1453
|
-
... fetch('Convallaria_m2_1740751781_phasor_ch1.json')
|
1454
|
-
... )
|
1455
|
-
>>> signal.values
|
1456
|
-
array(...)
|
1457
|
-
>>> signal.shape
|
1458
|
-
(256, 256, 256)
|
1459
|
-
>>> signal.dims
|
1460
|
-
('Y', 'X', 'H')
|
1461
|
-
>>> signal.coords['H'].data
|
1462
|
-
array(...)
|
1463
|
-
>>> signal.attrs['frequency'] # doctest: +NUMBER
|
1464
|
-
40.00
|
1465
|
-
|
1466
|
-
"""
|
1467
|
-
import json
|
1468
|
-
|
1469
|
-
with open(filename, 'rb') as fh:
|
1470
|
-
try:
|
1471
|
-
data = json.load(fh)
|
1472
|
-
except Exception as exc:
|
1473
|
-
raise ValueError('not a valid JSON file') from exc
|
1474
|
-
|
1475
|
-
if (
|
1476
|
-
'header' not in data
|
1477
|
-
or 'laser_period_ns' not in data['header']
|
1478
|
-
or 'file_id' not in data['header']
|
1479
|
-
or ('data' not in data and 'intensities_data' not in data)
|
1480
|
-
):
|
1481
|
-
raise ValueError(
|
1482
|
-
'not a FLIM LABS JSON file containing TCSPC histogram'
|
1483
|
-
)
|
1484
|
-
|
1485
|
-
if dtype is None:
|
1486
|
-
dtype = numpy.uint16
|
1487
|
-
else:
|
1488
|
-
dtype = numpy.dtype(dtype)
|
1489
|
-
if dtype.kind != 'u':
|
1490
|
-
raise ValueError(f'{dtype=} is not an unsigned integer type')
|
1491
|
-
|
1492
|
-
header = data['header']
|
1493
|
-
nchannels = len([c for c in header['channels'] if c])
|
1494
|
-
height = header['image_height']
|
1495
|
-
width = header['image_width']
|
1496
|
-
frequency = 1000.0 / header['laser_period_ns']
|
1497
|
-
|
1498
|
-
if channel is not None:
|
1499
|
-
if channel >= nchannels or channel < 0:
|
1500
|
-
raise IndexError(f'{channel=} out of range[0, {nchannels=}]')
|
1501
|
-
nchannels = 1
|
1502
|
-
|
1503
|
-
if 'data' in data:
|
1504
|
-
# file_id = [73, 77, 71, 49] # 'IMG1'
|
1505
|
-
intensities_data = data['data']
|
1506
|
-
else:
|
1507
|
-
# file_id = [73, 80, 71, 49] # 'IPG1'
|
1508
|
-
intensities_data = data['intensities_data']
|
1509
|
-
|
1510
|
-
from ._phasorpy import _flimlabs_signal
|
1511
|
-
|
1512
|
-
signal = numpy.zeros((nchannels, height * width, 256), dtype)
|
1513
|
-
_flimlabs_signal(
|
1514
|
-
signal,
|
1515
|
-
intensities_data,
|
1516
|
-
-1 if channel is None else channel,
|
1517
|
-
)
|
1518
|
-
|
1519
|
-
if channel is None and nchannels > 1:
|
1520
|
-
signal.shape = (nchannels, height, width, 256)
|
1521
|
-
axes = 'CYXH'
|
1522
|
-
else:
|
1523
|
-
signal.shape = (height, width, 256)
|
1524
|
-
axes = 'YXH'
|
1525
|
-
|
1526
|
-
coords: dict[str, Any] = {}
|
1527
|
-
coords['H'] = numpy.linspace(
|
1528
|
-
0.0, header['laser_period_ns'], 256, endpoint=False
|
1529
|
-
)
|
1530
|
-
if channel is None and nchannels > 1:
|
1531
|
-
coords['C'] = numpy.asarray(
|
1532
|
-
[i for i, c in enumerate(header['channels']) if c]
|
1533
|
-
)
|
1534
|
-
|
1535
|
-
metadata = _metadata(axes, signal.shape, filename, **coords)
|
1536
|
-
attrs = metadata['attrs']
|
1537
|
-
attrs['frequency'] = frequency
|
1538
|
-
attrs['flimlabs_header'] = header
|
1539
|
-
|
1540
|
-
from xarray import DataArray
|
1541
|
-
|
1542
|
-
return DataArray(signal, **metadata)
|
1543
|
-
|
1544
|
-
|
1545
|
-
def signal_from_lif(
|
1546
|
-
filename: str | PathLike[Any],
|
1547
|
-
/,
|
1548
|
-
*,
|
1549
|
-
image: int | str | None = None,
|
1550
|
-
dim: Literal['λ', 'Λ'] | str = 'λ',
|
1551
|
-
) -> DataArray:
|
1552
|
-
"""Return hyperspectral image and metadata from Leica image file.
|
1553
|
-
|
1554
|
-
Leica image files may contain hyperspectral images and metadata from laser
|
1555
|
-
scanning microscopy measurements.
|
1556
|
-
|
1557
|
-
Parameters
|
1558
|
-
----------
|
1559
|
-
filename : str or Path
|
1560
|
-
Name of Leica image file to read.
|
1561
|
-
image : str or int, optional
|
1562
|
-
Index or regex pattern of image to return.
|
1563
|
-
By default, return the first image containing hyperspectral data.
|
1564
|
-
dim : str or None
|
1565
|
-
Character code of hyperspectral dimension.
|
1566
|
-
Either ``'λ'`` for emission (default) or ``'Λ'`` for excitation.
|
1567
|
-
|
1568
|
-
Returns
|
1569
|
-
-------
|
1570
|
-
xarray.DataArray
|
1571
|
-
Hyperspectral image data.
|
1572
|
-
|
1573
|
-
- ``coords['C']``: wavelengths in nm.
|
1574
|
-
- ``coords['T']``: time coordinates in s, if any.
|
1575
|
-
|
1576
|
-
Raises
|
1577
|
-
------
|
1578
|
-
liffile.LifFileError
|
1579
|
-
File is not a Leica image file.
|
1580
|
-
ValueError
|
1581
|
-
File is not a Leica image file or does not contain hyperspectral image.
|
1582
|
-
|
1583
|
-
Notes
|
1584
|
-
-----
|
1585
|
-
The implementation is based on the
|
1586
|
-
`liffile <https://github.com/cgohlke/liffile/>`__ library.
|
1587
|
-
|
1588
|
-
Reading of TCSPC histograms from FLIM measurements is not supported
|
1589
|
-
because the compression scheme is patent-pending.
|
1590
|
-
|
1591
|
-
Examples
|
1592
|
-
--------
|
1593
|
-
>>> signal = signal_from_lif('ScanModesExamples.lif') # doctest: +SKIP
|
1594
|
-
>>> signal.values # doctest: +SKIP
|
1595
|
-
array(...)
|
1596
|
-
>>> signal.shape # doctest: +SKIP
|
1597
|
-
(9, 128, 128)
|
1598
|
-
>>> signal.dims # doctest: +SKIP
|
1599
|
-
('C', 'Y', 'X')
|
1600
|
-
>>> signal.coords['C'].data # doctest: +SKIP
|
1601
|
-
array([560, 580, 600, ..., 680, 700, 720])
|
1602
|
-
|
1603
|
-
"""
|
1604
|
-
import liffile
|
1605
|
-
|
1606
|
-
with liffile.LifFile(filename) as lif:
|
1607
|
-
if image is None:
|
1608
|
-
# find image with excitation or emission dimension
|
1609
|
-
for im in lif.images:
|
1610
|
-
if dim in im.dims:
|
1611
|
-
break
|
1612
|
-
else:
|
1613
|
-
raise ValueError(
|
1614
|
-
f'{lif.filename!r} does not contain hyperspectral image'
|
1615
|
-
)
|
1616
|
-
else:
|
1617
|
-
im = lif.images[image]
|
1618
|
-
|
1619
|
-
if dim not in im.dims or im.sizes[dim] < 4:
|
1620
|
-
raise ValueError(f'{im!r} does not contain spectral dimension')
|
1621
|
-
if 'C' in im.dims:
|
1622
|
-
raise ValueError(
|
1623
|
-
'hyperspectral image must not contain channel axis'
|
1624
|
-
)
|
1625
|
-
|
1626
|
-
data = im.asarray()
|
1627
|
-
coords: dict[str, Any] = {
|
1628
|
-
('C' if k == dim else k): (v * 1e9 if k == dim else v)
|
1629
|
-
for (k, v) in im.coords.items()
|
1630
|
-
}
|
1631
|
-
dims = tuple(('C' if d == dim else d) for d in im.dims)
|
1632
|
-
|
1633
|
-
metadata = _metadata(dims, im.shape, filename, **coords)
|
1634
|
-
|
1635
|
-
from xarray import DataArray
|
1636
|
-
|
1637
|
-
return DataArray(data, **metadata)
|
1638
|
-
|
1639
|
-
|
1640
|
-
def signal_from_lsm(
|
1641
|
-
filename: str | PathLike[Any],
|
1642
|
-
/,
|
1643
|
-
) -> DataArray:
|
1644
|
-
"""Return hyperspectral image and metadata from Zeiss LSM file.
|
1645
|
-
|
1646
|
-
LSM files contain multi-dimensional images and metadata from laser
|
1647
|
-
scanning microscopy measurements. The file format is based on TIFF.
|
1648
|
-
|
1649
|
-
Parameters
|
1650
|
-
----------
|
1651
|
-
filename : str or Path
|
1652
|
-
Name of Zeiss LSM file to read.
|
1653
|
-
|
1654
|
-
Returns
|
1655
|
-
-------
|
1656
|
-
xarray.DataArray
|
1657
|
-
Hyperspectral image data.
|
1658
|
-
Usually, a 3-to-5-dimensional array of type ``uint8`` or ``uint16``.
|
1659
|
-
|
1660
|
-
- ``coords['C']``: wavelengths in nm.
|
1661
|
-
- ``coords['T']``: time coordinates in s, if any.
|
1662
|
-
|
1663
|
-
Raises
|
1664
|
-
------
|
1665
|
-
tifffile.TiffFileError
|
1666
|
-
File is not a TIFF file.
|
1667
|
-
ValueError
|
1668
|
-
File is not an LSM file or does not contain hyperspectral image.
|
1669
|
-
|
1670
|
-
Notes
|
1671
|
-
-----
|
1672
|
-
The implementation is based on the
|
1673
|
-
`tifffile <https://github.com/cgohlke/tifffile/>`__ library.
|
1674
|
-
|
1675
|
-
Examples
|
1676
|
-
--------
|
1677
|
-
>>> signal = signal_from_lsm(fetch('paramecium.lsm'))
|
1678
|
-
>>> signal.values
|
1679
|
-
array(...)
|
1680
|
-
>>> signal.dtype
|
1681
|
-
dtype('uint8')
|
1682
|
-
>>> signal.shape
|
1683
|
-
(30, 512, 512)
|
1684
|
-
>>> signal.dims
|
1685
|
-
('C', 'Y', 'X')
|
1686
|
-
>>> signal.coords['C'].data # wavelengths
|
1687
|
-
array([423, ..., 713])
|
1688
|
-
|
1689
|
-
"""
|
1690
|
-
import tifffile
|
1691
|
-
|
1692
|
-
with tifffile.TiffFile(filename) as tif:
|
1693
|
-
if not tif.is_lsm:
|
1694
|
-
raise ValueError(f'{tif.filename!r} is not an LSM file')
|
1695
|
-
|
1696
|
-
page = tif.pages.first
|
1697
|
-
lsminfo = tif.lsm_metadata
|
1698
|
-
channels = page.tags[258].count
|
1699
|
-
|
1700
|
-
if channels < 4 or lsminfo is None or lsminfo['SpectralScan'] != 1:
|
1701
|
-
raise ValueError(
|
1702
|
-
f'{tif.filename!r} does not contain hyperspectral image'
|
1703
|
-
)
|
1704
|
-
|
1705
|
-
# TODO: contribute this to tifffile
|
1706
|
-
series = tif.series[0]
|
1707
|
-
data = series.asarray()
|
1708
|
-
dims = tuple(series.axes)
|
1709
|
-
coords = {}
|
1710
|
-
# channel wavelengths
|
1711
|
-
axis = dims.index('C')
|
1712
|
-
wavelengths = lsminfo['ChannelWavelength'].mean(axis=1)
|
1713
|
-
if wavelengths.size != data.shape[axis]:
|
1714
|
-
raise ValueError(
|
1715
|
-
f'{tif.filename!r} wavelengths do not match channel axis'
|
1716
|
-
)
|
1717
|
-
# stack may contain non-wavelength frame
|
1718
|
-
indices = wavelengths > 0
|
1719
|
-
wavelengths = wavelengths[indices]
|
1720
|
-
if wavelengths.size < 3:
|
1721
|
-
raise ValueError(
|
1722
|
-
f'{tif.filename!r} does not contain hyperspectral image'
|
1723
|
-
)
|
1724
|
-
wavelengths *= 1e9
|
1725
|
-
data = data.take(indices.nonzero()[0], axis=axis)
|
1726
|
-
coords['C'] = wavelengths
|
1727
|
-
# time stamps
|
1728
|
-
if 'T' in dims:
|
1729
|
-
coords['T'] = lsminfo['TimeStamps'] - lsminfo['TimeStamps'][0]
|
1730
|
-
if coords['T'].size != data.shape[dims.index('T')]:
|
1731
|
-
raise ValueError(
|
1732
|
-
f'{tif.filename!r} timestamps do not match time axis'
|
1733
|
-
)
|
1734
|
-
# spatial coordinates
|
1735
|
-
for ax in 'ZYX':
|
1736
|
-
if ax in dims:
|
1737
|
-
size = data.shape[dims.index(ax)]
|
1738
|
-
coords[ax] = numpy.linspace(
|
1739
|
-
lsminfo[f'Origin{ax}'],
|
1740
|
-
size * lsminfo[f'VoxelSize{ax}'],
|
1741
|
-
size,
|
1742
|
-
endpoint=False,
|
1743
|
-
dtype=numpy.float64,
|
1744
|
-
)
|
1745
|
-
metadata = _metadata(series.axes, data.shape, filename, **coords)
|
1746
|
-
|
1747
|
-
from xarray import DataArray
|
1748
|
-
|
1749
|
-
return DataArray(data, **metadata)
|
1750
|
-
|
1751
|
-
|
1752
|
-
def signal_from_imspector_tiff(
|
1753
|
-
filename: str | PathLike[Any],
|
1754
|
-
/,
|
1755
|
-
) -> DataArray:
|
1756
|
-
"""Return TCSPC histogram and metadata from ImSpector TIFF file.
|
1757
|
-
|
1758
|
-
Parameters
|
1759
|
-
----------
|
1760
|
-
filename : str or Path
|
1761
|
-
Name of ImSpector FLIM TIFF file to read.
|
1762
|
-
|
1763
|
-
Returns
|
1764
|
-
-------
|
1765
|
-
xarray.DataArray
|
1766
|
-
TCSPC histogram with :ref:`axes codes <axes>` ``'HTZYX'`` and
|
1767
|
-
type ``uint16``.
|
1768
|
-
|
1769
|
-
- ``coords['H']``: delay-times of histogram bins in ns.
|
1770
|
-
- ``attrs['frequency']``: repetition frequency in MHz.
|
1771
|
-
|
1772
|
-
Raises
|
1773
|
-
------
|
1774
|
-
tifffile.TiffFileError
|
1775
|
-
File is not a TIFF file.
|
1776
|
-
ValueError
|
1777
|
-
File is not an ImSpector FLIM TIFF file.
|
1778
|
-
|
1779
|
-
Notes
|
1780
|
-
-----
|
1781
|
-
The implementation is based on the
|
1782
|
-
`tifffile <https://github.com/cgohlke/tifffile/>`__ library.
|
1783
|
-
|
1784
|
-
Examples
|
1785
|
-
--------
|
1786
|
-
>>> signal = signal_from_imspector_tiff(fetch('Embryo.tif'))
|
1787
|
-
>>> signal.values
|
1788
|
-
array(...)
|
1789
|
-
>>> signal.dtype
|
1790
|
-
dtype('uint16')
|
1791
|
-
>>> signal.shape
|
1792
|
-
(56, 512, 512)
|
1793
|
-
>>> signal.dims
|
1794
|
-
('H', 'Y', 'X')
|
1795
|
-
>>> signal.coords['H'].data # dtime bins
|
1796
|
-
array([0, ..., 12.26])
|
1797
|
-
>>> signal.attrs['frequency'] # doctest: +NUMBER
|
1798
|
-
80.109
|
1799
|
-
|
1800
|
-
"""
|
1801
|
-
from xml.etree import ElementTree
|
1802
|
-
|
1803
|
-
import tifffile
|
1804
|
-
|
1805
|
-
with tifffile.TiffFile(filename) as tif:
|
1806
|
-
tags = tif.pages.first.tags
|
1807
|
-
omexml = tags.valueof(270, '')
|
1808
|
-
make = tags.valueof(271, '')
|
1809
|
-
|
1810
|
-
if (
|
1811
|
-
make != 'ImSpector'
|
1812
|
-
or not omexml.startswith('<?xml version')
|
1813
|
-
or len(tif.series) != 1
|
1814
|
-
or not tif.is_ome
|
1815
|
-
):
|
1816
|
-
raise ValueError(f'{tif.filename!r} is not an ImSpector TIFF file')
|
1817
|
-
|
1818
|
-
series = tif.series[0]
|
1819
|
-
ndim = series.ndim
|
1820
|
-
axes = series.axes
|
1821
|
-
shape = series.shape
|
1822
|
-
|
1823
|
-
if ndim < 3 or not axes.endswith('YX'):
|
1824
|
-
raise ValueError(
|
1825
|
-
f'{tif.filename!r} is not an ImSpector FLIM TIFF file'
|
1826
|
-
)
|
1827
|
-
|
1828
|
-
data = series.asarray()
|
1829
|
-
|
1830
|
-
attrs: dict[str, Any] = {}
|
1831
|
-
coords = {}
|
1832
|
-
physical_size = {}
|
1833
|
-
|
1834
|
-
root = ElementTree.fromstring(omexml)
|
1835
|
-
ns = {
|
1836
|
-
'': 'http://www.openmicroscopy.org/Schemas/OME/2008-02',
|
1837
|
-
'ca': 'http://www.openmicroscopy.org/Schemas/CA/2008-02',
|
1838
|
-
}
|
1839
|
-
|
1840
|
-
description = root.find('.//Description', ns)
|
1841
|
-
if (
|
1842
|
-
description is not None
|
1843
|
-
and description.text
|
1844
|
-
and description.text != 'not_specified'
|
1845
|
-
):
|
1846
|
-
attrs['description'] = description.text
|
1847
|
-
|
1848
|
-
pixels = root.find('.//Image/Pixels', ns)
|
1849
|
-
assert pixels is not None
|
1850
|
-
for ax in 'TZYX':
|
1851
|
-
attrib = 'TimeIncrement' if ax == 'T' else f'PhysicalSize{ax}'
|
1852
|
-
if ax not in axes or attrib not in pixels.attrib:
|
1853
|
-
continue
|
1854
|
-
size = float(pixels.attrib[attrib]) * shape[axes.index(ax)]
|
1855
|
-
physical_size[ax] = size
|
1856
|
-
coords[ax] = numpy.linspace(
|
1857
|
-
0.0,
|
1858
|
-
size,
|
1859
|
-
shape[axes.index(ax)],
|
1860
|
-
endpoint=False,
|
1861
|
-
dtype=numpy.float64,
|
1862
|
-
)
|
1863
|
-
|
1864
|
-
axes_labels = root.find('.//Image/ca:CustomAttributes/AxesLabels', ns)
|
1865
|
-
if (
|
1866
|
-
axes_labels is None
|
1867
|
-
or 'X' not in axes_labels.attrib
|
1868
|
-
or 'TCSPC' not in axes_labels.attrib['X']
|
1869
|
-
or 'FirstAxis' not in axes_labels.attrib
|
1870
|
-
or 'SecondAxis' not in axes_labels.attrib
|
1871
|
-
):
|
1872
|
-
raise ValueError(
|
1873
|
-
f'{tif.filename!r} is not an ImSpector FLIM TIFF file'
|
1874
|
-
)
|
1875
|
-
|
1876
|
-
if axes_labels.attrib['FirstAxis'] == 'lifetime' or axes_labels.attrib[
|
1877
|
-
'FirstAxis'
|
1878
|
-
].endswith('TCSPC T'):
|
1879
|
-
ax = axes[-3]
|
1880
|
-
assert axes_labels.attrib['FirstAxis-Unit'] == 'ns'
|
1881
|
-
elif ndim > 3 and (
|
1882
|
-
axes_labels.attrib['SecondAxis'] == 'lifetime'
|
1883
|
-
or axes_labels.attrib['SecondAxis'].endswith('TCSPC T')
|
1884
|
-
):
|
1885
|
-
ax = axes[-4]
|
1886
|
-
assert axes_labels.attrib['SecondAxis-Unit'] == 'ns'
|
1887
|
-
else:
|
1888
|
-
raise ValueError(
|
1889
|
-
f'{tif.filename!r} is not an ImSpector FLIM TIFF file'
|
1890
|
-
)
|
1891
|
-
axes = axes.replace(ax, 'H')
|
1892
|
-
coords['H'] = coords[ax]
|
1893
|
-
del coords[ax]
|
1894
|
-
|
1895
|
-
attrs['frequency'] = float(1000.0 / physical_size[ax])
|
1896
|
-
|
1897
|
-
metadata = _metadata(axes, shape, filename, attrs=attrs, **coords)
|
1898
|
-
|
1899
|
-
from xarray import DataArray
|
1900
|
-
|
1901
|
-
return DataArray(data, **metadata)
|
1902
|
-
|
1903
|
-
|
1904
|
-
def signal_from_sdt(
|
1905
|
-
filename: str | PathLike[Any],
|
1906
|
-
/,
|
1907
|
-
*,
|
1908
|
-
index: int = 0,
|
1909
|
-
) -> DataArray:
|
1910
|
-
"""Return TCSPC histogram and metadata from Becker & Hickl SDT file.
|
1911
|
-
|
1912
|
-
SDT files contain TCSPC measurement data and instrumentation parameters.
|
1913
|
-
|
1914
|
-
Parameters
|
1915
|
-
----------
|
1916
|
-
filename : str or Path
|
1917
|
-
Name of Becker & Hickl SDT file to read.
|
1918
|
-
index : int, optional, default: 0
|
1919
|
-
Index of dataset to read in case the file contains multiple datasets.
|
1920
|
-
|
1921
|
-
Returns
|
1922
|
-
-------
|
1923
|
-
xarray.DataArray
|
1924
|
-
TCSPC histogram with :ref:`axes codes <axes>` ``'QCYXH'`` and
|
1925
|
-
type ``uint16``, ``uint32``, or ``float32``.
|
1926
|
-
Dimensions ``'Q'`` and ``'C'`` are optional detector channels.
|
1927
|
-
|
1928
|
-
- ``coords['H']``: delay-times of histogram bins in ns.
|
1929
|
-
- ``attrs['frequency']``: repetition frequency in MHz.
|
1930
|
-
|
1931
|
-
Raises
|
1932
|
-
------
|
1933
|
-
ValueError
|
1934
|
-
File is not a SDT file containing TCSPC histogram.
|
1935
|
-
|
1936
|
-
Notes
|
1937
|
-
-----
|
1938
|
-
The implementation is based on the
|
1939
|
-
`sdtfile <https://github.com/cgohlke/sdtfile/>`__ library.
|
1940
|
-
|
1941
|
-
Examples
|
1942
|
-
--------
|
1943
|
-
>>> signal = signal_from_sdt(fetch('tcspc.sdt'))
|
1944
|
-
>>> signal.values
|
1945
|
-
array(...)
|
1946
|
-
>>> signal.dtype
|
1947
|
-
dtype('uint16')
|
1948
|
-
>>> signal.shape
|
1949
|
-
(128, 128, 256)
|
1950
|
-
>>> signal.dims
|
1951
|
-
('Y', 'X', 'H')
|
1952
|
-
>>> signal.coords['H'].data
|
1953
|
-
array([0, ..., 12.45])
|
1954
|
-
>>> signal.attrs['frequency'] # doctest: +NUMBER
|
1955
|
-
79.99
|
1956
|
-
|
1957
|
-
"""
|
1958
|
-
import sdtfile
|
1959
|
-
|
1960
|
-
with sdtfile.SdtFile(filename) as sdt:
|
1961
|
-
if (
|
1962
|
-
'SPC Setup & Data File' not in sdt.info.id
|
1963
|
-
and 'SPC FCS Data File' not in sdt.info.id
|
1964
|
-
):
|
1965
|
-
# skip DLL data
|
1966
|
-
raise ValueError(
|
1967
|
-
f'{os.path.basename(filename)!r} '
|
1968
|
-
'is not an SDT file containing TCSPC data'
|
1969
|
-
)
|
1970
|
-
# filter block types?
|
1971
|
-
# sdtfile.BlockType(sdt.block_headers[index].block_type).contents
|
1972
|
-
# == 'PAGE_BLOCK'
|
1973
|
-
data = sdt.data[index]
|
1974
|
-
times = sdt.times[index] * 1e9
|
1975
|
-
|
1976
|
-
# TODO: get spatial coordinates from scanner settings?
|
1977
|
-
metadata = _metadata('QCYXH'[-data.ndim :], data.shape, filename, H=times)
|
1978
|
-
metadata['attrs']['frequency'] = 1e3 / float(times[-1] + times[1])
|
1979
|
-
|
1980
|
-
from xarray import DataArray
|
1981
|
-
|
1982
|
-
return DataArray(data, **metadata)
|
1983
|
-
|
1984
|
-
|
1985
|
-
def signal_from_ptu(
|
1986
|
-
filename: str | PathLike[Any],
|
1987
|
-
/,
|
1988
|
-
selection: Sequence[int | slice | EllipsisType | None] | None = None,
|
1989
|
-
*,
|
1990
|
-
trimdims: Sequence[Literal['T', 'C', 'H']] | str | None = None,
|
1991
|
-
dtype: DTypeLike | None = None,
|
1992
|
-
frame: int | None = None,
|
1993
|
-
channel: int | None = None,
|
1994
|
-
dtime: int | None = 0,
|
1995
|
-
keepdims: bool = True,
|
1996
|
-
) -> DataArray:
|
1997
|
-
"""Return TCSPC histogram and metadata from PicoQuant PTU T3 mode file.
|
1998
|
-
|
1999
|
-
PTU files contain TCSPC measurement data and instrumentation parameters,
|
2000
|
-
which are decoded to a multi-dimensional TCSPC histogram.
|
2001
|
-
|
2002
|
-
Parameters
|
2003
|
-
----------
|
2004
|
-
filename : str or Path
|
2005
|
-
Name of PTU file to read.
|
2006
|
-
selection : sequence of index types, optional
|
2007
|
-
Indices for all dimensions of image mode files:
|
2008
|
-
|
2009
|
-
- ``None``: return all items along axis (default).
|
2010
|
-
- ``Ellipsis``: return all items along multiple axes.
|
2011
|
-
- ``int``: return single item along axis.
|
2012
|
-
- ``slice``: return chunk of axis.
|
2013
|
-
``slice.step`` is a binning factor.
|
2014
|
-
If ``slice.step=-1``, integrate all items along axis.
|
2015
|
-
|
2016
|
-
trimdims : str, optional, default: 'TCH'
|
2017
|
-
Axes to trim.
|
2018
|
-
dtype : dtype-like, optional, default: uint16
|
2019
|
-
Unsigned integer type of TCSPC histogram.
|
2020
|
-
Increase the bit depth to avoid overflows when integrating.
|
2021
|
-
frame : int, optional
|
2022
|
-
If < 0, integrate time axis, else return specified frame.
|
2023
|
-
Overrides `selection` for axis ``T``.
|
2024
|
-
channel : int, optional
|
2025
|
-
If < 0, integrate channel axis, else return specified channel.
|
2026
|
-
Overrides `selection` for axis ``C``.
|
2027
|
-
dtime : int, optional, default: 0
|
2028
|
-
Specifies number of bins in TCSPC histogram.
|
2029
|
-
If 0 (default), return the number of bins in one period.
|
2030
|
-
If < 0, integrate delay-time axis (image mode only).
|
2031
|
-
If > 0, return up to specified bin.
|
2032
|
-
Overrides `selection` for axis ``H``.
|
2033
|
-
keepdims : bool, optional, default: True
|
2034
|
-
If true (default), reduced axes are left as size-one dimension.
|
2035
|
-
|
2036
|
-
Returns
|
2037
|
-
-------
|
2038
|
-
xarray.DataArray
|
2039
|
-
TCSPC histogram with :ref:`axes codes <axes>` ``'TYXCH'`` and
|
2040
|
-
type specified in ``dtype``:
|
2041
|
-
|
2042
|
-
- ``coords['H']``: delay-times of histogram bins in ns.
|
2043
|
-
- ``attrs['frequency']``: repetition frequency in MHz.
|
2044
|
-
- ``attrs['ptu_tags']``: metadata read from PTU file.
|
2045
|
-
|
2046
|
-
Size-one dimensions are prepended to point mode data to make them
|
2047
|
-
broadcastable to image data.
|
2048
|
-
|
2049
|
-
Raises
|
2050
|
-
------
|
2051
|
-
ptufile.PqFileError
|
2052
|
-
File is not a PicoQuant PTU file or is corrupted.
|
2053
|
-
ValueError
|
2054
|
-
File is not a PicoQuant PTU T3 mode file containing TCSPC data.
|
2055
|
-
|
2056
|
-
Notes
|
2057
|
-
-----
|
2058
|
-
The implementation is based on the
|
2059
|
-
`ptufile <https://github.com/cgohlke/ptufile/>`__ library.
|
2060
|
-
|
2061
|
-
Examples
|
2062
|
-
--------
|
2063
|
-
>>> signal = signal_from_ptu(fetch('hazelnut_FLIM_single_image.ptu'))
|
2064
|
-
>>> signal.values
|
2065
|
-
array(...)
|
2066
|
-
>>> signal.dtype
|
2067
|
-
dtype('uint16')
|
2068
|
-
>>> signal.shape
|
2069
|
-
(5, 256, 256, 1, 132)
|
2070
|
-
>>> signal.dims
|
2071
|
-
('T', 'Y', 'X', 'C', 'H')
|
2072
|
-
>>> signal.coords['H'].data
|
2073
|
-
array([0, ..., 12.7])
|
2074
|
-
>>> signal.attrs['frequency'] # doctest: +NUMBER
|
2075
|
-
78.02
|
2076
|
-
|
2077
|
-
"""
|
2078
|
-
import ptufile
|
2079
|
-
from xarray import DataArray
|
2080
|
-
|
2081
|
-
with ptufile.PtuFile(filename, trimdims=trimdims) as ptu:
|
2082
|
-
if not ptu.is_t3:
|
2083
|
-
raise ValueError(f'{ptu.filename!r} is not a T3 mode PTU file')
|
2084
|
-
if ptu.is_image:
|
2085
|
-
data = ptu.decode_image(
|
2086
|
-
selection,
|
2087
|
-
dtype=dtype,
|
2088
|
-
frame=frame,
|
2089
|
-
channel=channel,
|
2090
|
-
dtime=dtime,
|
2091
|
-
keepdims=keepdims,
|
2092
|
-
asxarray=True,
|
2093
|
-
)
|
2094
|
-
assert isinstance(data, DataArray)
|
2095
|
-
elif ptu.measurement_submode == 1:
|
2096
|
-
# point mode IRF
|
2097
|
-
if dtime == -1:
|
2098
|
-
raise ValueError(f'{dtime=} not supported for point mode')
|
2099
|
-
data = ptu.decode_histogram(
|
2100
|
-
dtype=dtype, dtime=dtime, asxarray=True
|
2101
|
-
)
|
2102
|
-
assert isinstance(data, DataArray)
|
2103
|
-
if channel is not None:
|
2104
|
-
if keepdims:
|
2105
|
-
data = data[channel : channel + 1]
|
2106
|
-
else:
|
2107
|
-
data = data[channel]
|
2108
|
-
# prepend dimensions as needed to appear image-like
|
2109
|
-
data = data.expand_dims(dim={'Y': 1, 'X': 1})
|
2110
|
-
if keepdims:
|
2111
|
-
data = data.expand_dims(dim={'T': 1})
|
2112
|
-
else:
|
2113
|
-
raise ValueError(
|
2114
|
-
f'{ptu.filename!r} is not a point or image mode PTU file'
|
2115
|
-
)
|
2116
|
-
|
2117
|
-
data.attrs['ptu_tags'] = ptu.tags
|
2118
|
-
data.attrs['frequency'] = ptu.frequency * 1e-6 # MHz
|
2119
|
-
data.coords['H'] = data.coords['H'] * 1e9
|
2120
|
-
|
2121
|
-
return data
|
2122
|
-
|
2123
|
-
|
2124
|
-
def signal_from_flif(
|
2125
|
-
filename: str | PathLike[Any],
|
2126
|
-
/,
|
2127
|
-
) -> DataArray:
|
2128
|
-
"""Return phase images and metadata from FlimFast FLIF file.
|
2129
|
-
|
2130
|
-
FlimFast FLIF files contain phase images and metadata from full-field,
|
2131
|
-
frequency-domain fluorescence lifetime measurements.
|
2132
|
-
|
2133
|
-
Parameters
|
2134
|
-
----------
|
2135
|
-
filename : str or Path
|
2136
|
-
Name of FlimFast FLIF file to read.
|
2137
|
-
|
2138
|
-
Returns
|
2139
|
-
-------
|
2140
|
-
xarray.DataArray
|
2141
|
-
Phase images with :ref:`axes codes <axes>` ``'THYX'`` and
|
2142
|
-
type ``uint16``:
|
2143
|
-
|
2144
|
-
- ``coords['H']``: phases in radians.
|
2145
|
-
- ``attrs['frequency']``: repetition frequency in MHz.
|
2146
|
-
- ``attrs['ref_phase']``: measured phase of reference.
|
2147
|
-
- ``attrs['ref_mod']``: measured modulation of reference.
|
2148
|
-
- ``attrs['ref_tauphase']``: lifetime from phase of reference in ns.
|
2149
|
-
- ``attrs['ref_taumod']``: lifetime from modulation of reference in ns.
|
2150
|
-
|
2151
|
-
Raises
|
2152
|
-
------
|
2153
|
-
lfdfiles.LfdFileError
|
2154
|
-
File is not a FlimFast FLIF file.
|
2155
|
-
|
2156
|
-
Notes
|
2157
|
-
-----
|
2158
|
-
The implementation is based on the
|
2159
|
-
`lfdfiles <https://github.com/cgohlke/lfdfiles/>`__ library.
|
2160
|
-
|
2161
|
-
Examples
|
2162
|
-
--------
|
2163
|
-
>>> signal = signal_from_flif(fetch('flimfast.flif'))
|
2164
|
-
>>> signal.values
|
2165
|
-
array(...)
|
2166
|
-
>>> signal.dtype
|
2167
|
-
dtype('uint16')
|
2168
|
-
>>> signal.shape
|
2169
|
-
(32, 220, 300)
|
2170
|
-
>>> signal.dims
|
2171
|
-
('H', 'Y', 'X')
|
2172
|
-
>>> signal.coords['H'].data
|
2173
|
-
array([0, ..., 6.087], dtype=float32)
|
2174
|
-
>>> signal.attrs['frequency'] # doctest: +NUMBER
|
2175
|
-
80.65
|
2176
|
-
|
2177
|
-
"""
|
2178
|
-
import lfdfiles
|
2179
|
-
|
2180
|
-
with lfdfiles.FlimfastFlif(filename) as flif:
|
2181
|
-
nphases = int(flif.header.phases)
|
2182
|
-
data = flif.asarray()
|
2183
|
-
if data.shape[0] < nphases:
|
2184
|
-
raise ValueError(f'measured phases {data.shape[0]} < {nphases=}')
|
2185
|
-
if data.shape[0] % nphases != 0:
|
2186
|
-
data = data[: (data.shape[0] // nphases) * nphases]
|
2187
|
-
data = data.reshape(-1, nphases, data.shape[1], data.shape[2])
|
2188
|
-
if data.shape[0] == 1:
|
2189
|
-
data = data[0]
|
2190
|
-
axes = 'HYX'
|
2191
|
-
else:
|
2192
|
-
axes = 'THYX'
|
2193
|
-
# TODO: check if phases are ordered
|
2194
|
-
phases = numpy.radians(flif.records['phase'][:nphases])
|
2195
|
-
metadata = _metadata(axes, data.shape, H=phases)
|
2196
|
-
attrs = metadata['attrs']
|
2197
|
-
attrs['frequency'] = float(flif.header.frequency)
|
2198
|
-
attrs['ref_phase'] = float(flif.header.measured_phase)
|
2199
|
-
attrs['ref_mod'] = float(flif.header.measured_mod)
|
2200
|
-
attrs['ref_tauphase'] = float(flif.header.ref_tauphase)
|
2201
|
-
attrs['ref_taumod'] = float(flif.header.ref_taumod)
|
2202
|
-
|
2203
|
-
from xarray import DataArray
|
2204
|
-
|
2205
|
-
return DataArray(data, **metadata)
|
2206
|
-
|
2207
|
-
|
2208
|
-
def signal_from_fbd(
|
2209
|
-
filename: str | PathLike[Any],
|
2210
|
-
/,
|
2211
|
-
*,
|
2212
|
-
frame: int | None = None,
|
2213
|
-
channel: int | None = None,
|
2214
|
-
keepdims: bool = True,
|
2215
|
-
laser_factor: float = -1.0,
|
2216
|
-
) -> DataArray:
|
2217
|
-
"""Return phase histogram and metadata from FLIMbox FBD file.
|
2218
|
-
|
2219
|
-
FDB files contain encoded cross-correlation phase histograms from
|
2220
|
-
digital frequency-domain measurements using a FLIMbox device.
|
2221
|
-
The encoding scheme depends on the FLIMbox device's firmware.
|
2222
|
-
The FBD file format is undocumented.
|
2223
|
-
|
2224
|
-
This function may fail to produce expected results when files use unknown
|
2225
|
-
firmware, do not contain image scans, settings were recorded incorrectly,
|
2226
|
-
scanner and FLIMbox frequencies were out of sync, or scanner settings were
|
2227
|
-
changed during acquisition.
|
2228
|
-
|
2229
|
-
Parameters
|
2230
|
-
----------
|
2231
|
-
filename : str or Path
|
2232
|
-
Name of FLIMbox FBD file to read.
|
2233
|
-
frame : int, optional
|
2234
|
-
If None (default), return all frames.
|
2235
|
-
If < 0, integrate time axis, else return specified frame.
|
2236
|
-
channel : int, optional
|
2237
|
-
If None (default), return all channels, else return specified channel.
|
2238
|
-
keepdims : bool, optional
|
2239
|
-
If true (default), reduced axes are left as size-one dimension.
|
2240
|
-
laser_factor : float, optional
|
2241
|
-
Factor to correct dwell_time/laser_frequency.
|
2242
|
-
|
2243
|
-
Returns
|
2244
|
-
-------
|
2245
|
-
xarray.DataArray
|
2246
|
-
Phase histogram with :ref:`axes codes <axes>` ``'TCYXH'`` and
|
2247
|
-
type ``uint16``:
|
2248
|
-
|
2249
|
-
- ``coords['H']``: cross-correlation phases in radians.
|
2250
|
-
- ``attrs['frequency']``: repetition frequency in MHz.
|
2251
|
-
- ``attrs['harmonic']``: harmonic contained in phase histogram.
|
2252
|
-
- ``attrs['flimbox_header']``: FBD binary header, if any.
|
2253
|
-
- ``attrs['flimbox_firmware']``: FLIMbox firmware settings, if any.
|
2254
|
-
- ``attrs['flimbox_settings']``: Settings from FBS XML, if any.
|
2255
|
-
|
2256
|
-
Raises
|
2257
|
-
------
|
2258
|
-
lfdfiles.LfdFileError
|
2259
|
-
File is not a FLIMbox FBD file.
|
2260
|
-
|
2261
|
-
Notes
|
2262
|
-
-----
|
2263
|
-
The implementation is based on the
|
2264
|
-
`lfdfiles <https://github.com/cgohlke/lfdfiles/>`__ library.
|
2265
|
-
|
2266
|
-
Examples
|
2267
|
-
--------
|
2268
|
-
>>> signal = signal_from_fbd(
|
2269
|
-
... fetch('convallaria_000$EI0S.fbd')
|
2270
|
-
... ) # doctest: +SKIP
|
2271
|
-
>>> signal.values # doctest: +SKIP
|
2272
|
-
array(...)
|
2273
|
-
>>> signal.dtype # doctest: +SKIP
|
2274
|
-
dtype('uint16')
|
2275
|
-
>>> signal.shape # doctest: +SKIP
|
2276
|
-
(9, 2, 256, 256, 64)
|
2277
|
-
>>> signal.dims # doctest: +SKIP
|
2278
|
-
('T', 'C', 'Y', 'X', 'H')
|
2279
|
-
>>> signal.coords['H'].data # doctest: +SKIP
|
2280
|
-
array([0, ..., 6.185])
|
2281
|
-
>>> signal.attrs['frequency'] # doctest: +SKIP
|
2282
|
-
40.0
|
2283
|
-
|
2284
|
-
"""
|
2285
|
-
import lfdfiles
|
2286
|
-
|
2287
|
-
integrate_frames = 0 if frame is None or frame >= 0 else 1
|
2288
|
-
|
2289
|
-
with lfdfiles.FlimboxFbd(filename, laser_factor=laser_factor) as fbd:
|
2290
|
-
data = fbd.asimage(None, None, integrate_frames=integrate_frames)
|
2291
|
-
if integrate_frames:
|
2292
|
-
frame = None
|
2293
|
-
copy = False
|
2294
|
-
axes = 'TCYXH'
|
2295
|
-
if channel is None:
|
2296
|
-
if not keepdims and data.shape[1] == 1:
|
2297
|
-
data = data[:, 0]
|
2298
|
-
axes = 'TYXH'
|
2299
|
-
else:
|
2300
|
-
if channel < 0 or channel >= data.shape[1]:
|
2301
|
-
raise IndexError(f'{channel=} out of bounds')
|
2302
|
-
if keepdims:
|
2303
|
-
data = data[:, channel : channel + 1]
|
2304
|
-
else:
|
2305
|
-
data = data[:, channel]
|
2306
|
-
axes = 'TYXH'
|
2307
|
-
copy = True
|
2308
|
-
if frame is None:
|
2309
|
-
if not keepdims and data.shape[0] == 1:
|
2310
|
-
data = data[0]
|
2311
|
-
axes = axes[1:]
|
2312
|
-
else:
|
2313
|
-
if frame < 0 or frame >= data.shape[0]:
|
2314
|
-
raise IndexError(f'{frame=} out of bounds')
|
2315
|
-
if keepdims:
|
2316
|
-
data = data[frame : frame + 1]
|
2317
|
-
else:
|
2318
|
-
data = data[frame]
|
2319
|
-
axes = axes[1:]
|
2320
|
-
copy = True
|
2321
|
-
if copy:
|
2322
|
-
data = data.copy()
|
2323
|
-
# TODO: return arrival window indices or micro-times as H coords?
|
2324
|
-
phases = numpy.linspace(
|
2325
|
-
0.0, numpy.pi * 2, data.shape[-1], endpoint=False
|
2326
|
-
)
|
2327
|
-
metadata = _metadata(axes, data.shape, H=phases)
|
2328
|
-
attrs = metadata['attrs']
|
2329
|
-
attrs['frequency'] = fbd.laser_frequency * 1e-6
|
2330
|
-
attrs['harmonic'] = fbd.harmonics
|
2331
|
-
if fbd.header is not None:
|
2332
|
-
attrs['flimbox_header'] = fbd.header
|
2333
|
-
if fbd.fbf is not None:
|
2334
|
-
attrs['flimbox_firmware'] = fbd.fbf
|
2335
|
-
if fbd.fbs is not None:
|
2336
|
-
attrs['flimbox_settings'] = fbd.fbs
|
2337
|
-
|
2338
|
-
from xarray import DataArray
|
2339
|
-
|
2340
|
-
return DataArray(data, **metadata)
|
2341
|
-
|
2342
|
-
|
2343
|
-
def signal_from_b64(
|
2344
|
-
filename: str | PathLike[Any],
|
2345
|
-
/,
|
2346
|
-
) -> DataArray:
|
2347
|
-
"""Return intensity image and metadata from SimFCS B64 file.
|
2348
|
-
|
2349
|
-
B64 files contain one or more square intensity image(s), a carpet
|
2350
|
-
of lines, or a stream of intensity data. B64 files contain no metadata.
|
2351
|
-
|
2352
|
-
Parameters
|
2353
|
-
----------
|
2354
|
-
filename : str or Path
|
2355
|
-
Name of SimFCS B64 file to read.
|
2356
|
-
|
2357
|
-
Returns
|
2358
|
-
-------
|
2359
|
-
xarray.DataArray
|
2360
|
-
Intensity image of type ``int16``.
|
2361
|
-
|
2362
|
-
Raises
|
2363
|
-
------
|
2364
|
-
lfdfiles.LfdFileError
|
2365
|
-
File is not a SimFCS B64 file.
|
2366
|
-
ValueError
|
2367
|
-
File does not contain an image stack.
|
2368
|
-
|
2369
|
-
Notes
|
2370
|
-
-----
|
2371
|
-
The implementation is based on the
|
2372
|
-
`lfdfiles <https://github.com/cgohlke/lfdfiles/>`__ library.
|
2373
|
-
|
2374
|
-
Examples
|
2375
|
-
--------
|
2376
|
-
>>> signal = signal_from_b64(fetch('simfcs.b64'))
|
2377
|
-
>>> signal.values
|
2378
|
-
array(...)
|
2379
|
-
>>> signal.dtype
|
2380
|
-
dtype('int16')
|
2381
|
-
>>> signal.shape
|
2382
|
-
(22, 1024, 1024)
|
2383
|
-
>>> signal.dtype
|
2384
|
-
dtype('int16')
|
2385
|
-
>>> signal.dims
|
2386
|
-
('I', 'Y', 'X')
|
2387
|
-
|
2388
|
-
"""
|
2389
|
-
import lfdfiles
|
2390
|
-
|
2391
|
-
with lfdfiles.SimfcsB64(filename) as b64:
|
2392
|
-
data = b64.asarray()
|
2393
|
-
if data.ndim != 3:
|
2394
|
-
raise ValueError(
|
2395
|
-
f'{os.path.basename(filename)!r} '
|
2396
|
-
'does not contain an image stack'
|
2397
|
-
)
|
2398
|
-
metadata = _metadata(b64.axes, data.shape, filename)
|
2399
|
-
|
2400
|
-
from xarray import DataArray
|
2401
|
-
|
2402
|
-
return DataArray(data, **metadata)
|
2403
|
-
|
2404
|
-
|
2405
|
-
def signal_from_z64(
|
2406
|
-
filename: str | PathLike[Any],
|
2407
|
-
/,
|
2408
|
-
) -> DataArray:
|
2409
|
-
"""Return image stack and metadata from SimFCS Z64 file.
|
2410
|
-
|
2411
|
-
Z64 files commonly contain stacks of square images, such as intensity
|
2412
|
-
volumes or TCSPC histograms. Z64 files contain no metadata.
|
2413
|
-
|
2414
|
-
Parameters
|
2415
|
-
----------
|
2416
|
-
filename : str or Path
|
2417
|
-
Name of SimFCS Z64 file to read.
|
2418
|
-
|
2419
|
-
Returns
|
2420
|
-
-------
|
2421
|
-
xarray.DataArray
|
2422
|
-
Image stack of type ``float32``.
|
2423
|
-
|
2424
|
-
Raises
|
2425
|
-
------
|
2426
|
-
lfdfiles.LfdFileError
|
2427
|
-
File is not a SimFCS Z64 file.
|
2428
|
-
|
2429
|
-
Notes
|
2430
|
-
-----
|
2431
|
-
The implementation is based on the
|
2432
|
-
`lfdfiles <https://github.com/cgohlke/lfdfiles/>`__ library.
|
2433
|
-
|
2434
|
-
Examples
|
2435
|
-
--------
|
2436
|
-
>>> signal = signal_from_z64(fetch('simfcs.z64'))
|
2437
|
-
>>> signal.values
|
2438
|
-
array(...)
|
2439
|
-
>>> signal.dtype
|
2440
|
-
dtype('float32')
|
2441
|
-
>>> signal.shape
|
2442
|
-
(256, 256, 256)
|
2443
|
-
>>> signal.dims
|
2444
|
-
('Q', 'Y', 'X')
|
2445
|
-
|
2446
|
-
"""
|
2447
|
-
import lfdfiles
|
2448
|
-
|
2449
|
-
with lfdfiles.SimfcsZ64(filename) as z64:
|
2450
|
-
data = z64.asarray()
|
2451
|
-
metadata = _metadata(z64.axes, data.shape, filename)
|
2452
|
-
|
2453
|
-
from xarray import DataArray
|
2454
|
-
|
2455
|
-
return DataArray(data, **metadata)
|
2456
|
-
|
2457
|
-
|
2458
|
-
def signal_from_bh(
|
2459
|
-
filename: str | PathLike[Any],
|
2460
|
-
/,
|
2461
|
-
) -> DataArray:
|
2462
|
-
"""Return TCSPC histogram and metadata from SimFCS B&H file.
|
2463
|
-
|
2464
|
-
B&H files contain TCSPC histograms acquired from Becker & Hickl
|
2465
|
-
cards, or converted from other data sources. B&H files contain no metadata.
|
2466
|
-
|
2467
|
-
Parameters
|
2468
|
-
----------
|
2469
|
-
filename : str or Path
|
2470
|
-
Name of SimFCS B&H file to read.
|
2471
|
-
|
2472
|
-
Returns
|
2473
|
-
-------
|
2474
|
-
xarray.DataArray
|
2475
|
-
TCSPC histogram with ref:`axes codes <axes>` ``'HYX'``,
|
2476
|
-
shape ``(256, 256, 256)``, and type ``float32``.
|
2477
|
-
|
2478
|
-
Raises
|
2479
|
-
------
|
2480
|
-
lfdfiles.LfdFileError
|
2481
|
-
File is not a SimFCS B&H file.
|
2482
|
-
|
2483
|
-
Notes
|
2484
|
-
-----
|
2485
|
-
The implementation is based on the
|
2486
|
-
`lfdfiles <https://github.com/cgohlke/lfdfiles/>`__ library.
|
2487
|
-
|
2488
|
-
Examples
|
2489
|
-
--------
|
2490
|
-
>>> signal = signal_from_bh(fetch('simfcs.b&h'))
|
2491
|
-
>>> signal.values
|
2492
|
-
array(...)
|
2493
|
-
>>> signal.dtype
|
2494
|
-
dtype('float32')
|
2495
|
-
>>> signal.shape
|
2496
|
-
(256, 256, 256)
|
2497
|
-
>>> signal.dims
|
2498
|
-
('H', 'Y', 'X')
|
2499
|
-
|
2500
|
-
"""
|
2501
|
-
import lfdfiles
|
2502
|
-
|
2503
|
-
with lfdfiles.SimfcsBh(filename) as bnh:
|
2504
|
-
assert bnh.axes is not None
|
2505
|
-
data = bnh.asarray()
|
2506
|
-
metadata = _metadata(bnh.axes.replace('Q', 'H'), data.shape, filename)
|
2507
|
-
|
2508
|
-
from xarray import DataArray
|
2509
|
-
|
2510
|
-
return DataArray(data, **metadata)
|
2511
|
-
|
2512
|
-
|
2513
|
-
def signal_from_bhz(
|
2514
|
-
filename: str | PathLike[Any],
|
2515
|
-
/,
|
2516
|
-
) -> DataArray:
|
2517
|
-
"""Return TCSPC histogram and metadata from SimFCS BHZ file.
|
2518
|
-
|
2519
|
-
BHZ files contain TCSPC histograms acquired from Becker & Hickl
|
2520
|
-
cards, or converted from other data sources. BHZ files contain no metadata.
|
2521
|
-
|
2522
|
-
Parameters
|
2523
|
-
----------
|
2524
|
-
filename : str or Path
|
2525
|
-
Name of SimFCS BHZ file to read.
|
2526
|
-
|
2527
|
-
Returns
|
2528
|
-
-------
|
2529
|
-
xarray.DataArray
|
2530
|
-
TCSPC histogram with ref:`axes codes <axes>` ``'HYX'``,
|
2531
|
-
shape ``(256, 256, 256)``, and type ``float32``.
|
2532
|
-
|
2533
|
-
Raises
|
2534
|
-
------
|
2535
|
-
lfdfiles.LfdFileError
|
2536
|
-
File is not a SimFCS BHZ file.
|
2537
|
-
|
2538
|
-
Notes
|
2539
|
-
-----
|
2540
|
-
The implementation is based on the
|
2541
|
-
`lfdfiles <https://github.com/cgohlke/lfdfiles/>`__ library.
|
2542
|
-
|
2543
|
-
Examples
|
2544
|
-
--------
|
2545
|
-
>>> signal = signal_from_bhz(fetch('simfcs.bhz'))
|
2546
|
-
>>> signal.values
|
2547
|
-
array(...)
|
2548
|
-
>>> signal.dtype
|
2549
|
-
dtype('float32')
|
2550
|
-
>>> signal.shape
|
2551
|
-
(256, 256, 256)
|
2552
|
-
>>> signal.dims
|
2553
|
-
('H', 'Y', 'X')
|
2554
|
-
|
2555
|
-
"""
|
2556
|
-
import lfdfiles
|
2557
|
-
|
2558
|
-
with lfdfiles.SimfcsBhz(filename) as bhz:
|
2559
|
-
assert bhz.axes is not None
|
2560
|
-
data = bhz.asarray()
|
2561
|
-
metadata = _metadata(bhz.axes.replace('Q', 'H'), data.shape, filename)
|
2562
|
-
|
2563
|
-
from xarray import DataArray
|
2564
|
-
|
2565
|
-
return DataArray(data, **metadata)
|
2566
|
-
|
2567
|
-
|
2568
|
-
def _metadata(
|
2569
|
-
dims: Sequence[str] | None,
|
2570
|
-
shape: tuple[int, ...],
|
2571
|
-
/,
|
2572
|
-
name: str | PathLike[Any] | None = None,
|
2573
|
-
attrs: dict[str, Any] | None = None,
|
2574
|
-
**coords: Any,
|
2575
|
-
) -> dict[str, Any]:
|
2576
|
-
"""Return xarray-style dims, coords, and attrs in a dict.
|
2577
|
-
|
2578
|
-
>>> _metadata('SYX', (3, 2, 1), S=['0', '1', '2'])
|
2579
|
-
{'dims': ('S', 'Y', 'X'), 'coords': {'S': ['0', '1', '2']}, 'attrs': {}}
|
2580
|
-
|
2581
|
-
"""
|
2582
|
-
assert dims is not None
|
2583
|
-
dims = tuple(dims)
|
2584
|
-
if len(dims) != len(shape):
|
2585
|
-
raise ValueError(
|
2586
|
-
f'dims do not match shape {len(dims)} != {len(shape)}'
|
2587
|
-
)
|
2588
|
-
coords = {dim: coords[dim] for dim in dims if dim in coords}
|
2589
|
-
if attrs is None:
|
2590
|
-
attrs = {}
|
2591
|
-
metadata = {'dims': dims, 'coords': coords, 'attrs': attrs}
|
2592
|
-
if name:
|
2593
|
-
metadata['name'] = os.path.basename(name)
|
2594
|
-
return metadata
|
2595
|
-
|
2596
|
-
|
2597
|
-
def _squeeze_dims(
|
2598
|
-
shape: Sequence[int],
|
2599
|
-
dims: Sequence[str],
|
2600
|
-
/,
|
2601
|
-
skip: Container[str] = 'XY',
|
2602
|
-
) -> tuple[tuple[int, ...], tuple[str, ...], tuple[bool, ...]]:
|
2603
|
-
"""Return shape and axes with length-1 dimensions removed.
|
2604
|
-
|
2605
|
-
Remove unused dimensions unless their axes are listed in the `skip`
|
2606
|
-
parameter.
|
2607
|
-
|
2608
|
-
Adapted from the tifffile library.
|
2609
|
-
|
2610
|
-
Parameters
|
2611
|
-
----------
|
2612
|
-
shape : tuple of ints
|
2613
|
-
Sequence of dimension sizes.
|
2614
|
-
dims : sequence of str
|
2615
|
-
Character codes for dimensions in `shape`.
|
2616
|
-
skip : container of str, optional
|
2617
|
-
Character codes for dimensions whose length-1 dimensions are
|
2618
|
-
not removed. The default is 'XY'.
|
2619
|
-
|
2620
|
-
Returns
|
2621
|
-
-------
|
2622
|
-
shape : tuple of ints
|
2623
|
-
Sequence of dimension sizes with length-1 dimensions removed.
|
2624
|
-
dims : tuple of str
|
2625
|
-
Character codes for dimensions in output `shape`.
|
2626
|
-
squeezed : str
|
2627
|
-
Dimensions were kept (True) or removed (False).
|
2628
|
-
|
2629
|
-
Examples
|
2630
|
-
--------
|
2631
|
-
>>> _squeeze_dims((5, 1, 2, 1, 1), 'TZYXC')
|
2632
|
-
((5, 2, 1), ('T', 'Y', 'X'), (True, False, True, True, False))
|
2633
|
-
>>> _squeeze_dims((1,), ('Q',))
|
2634
|
-
((1,), ('Q',), (True,))
|
2635
|
-
|
2636
|
-
"""
|
2637
|
-
if len(shape) != len(dims):
|
2638
|
-
raise ValueError(f'{len(shape)=} != {len(dims)=}')
|
2639
|
-
if not dims:
|
2640
|
-
return tuple(shape), tuple(dims), ()
|
2641
|
-
squeezed: list[bool] = []
|
2642
|
-
shape_squeezed: list[int] = []
|
2643
|
-
dims_squeezed: list[str] = []
|
2644
|
-
for size, ax in zip(shape, dims):
|
2645
|
-
if size > 1 or ax in skip:
|
2646
|
-
squeezed.append(True)
|
2647
|
-
shape_squeezed.append(size)
|
2648
|
-
dims_squeezed.append(ax)
|
2649
|
-
else:
|
2650
|
-
squeezed.append(False)
|
2651
|
-
if len(shape_squeezed) == 0:
|
2652
|
-
squeezed[-1] = True
|
2653
|
-
shape_squeezed.append(shape[-1])
|
2654
|
-
dims_squeezed.append(dims[-1])
|
2655
|
-
return tuple(shape_squeezed), tuple(dims_squeezed), tuple(squeezed)
|