phasorpy 0.5__cp312-cp312-win_amd64.whl → 0.7__cp312-cp312-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- phasorpy/__init__.py +2 -3
- phasorpy/_phasorpy.cp312-win_amd64.pyd +0 -0
- phasorpy/_phasorpy.pyx +466 -11
- phasorpy/_utils.py +222 -37
- phasorpy/cli.py +74 -3
- phasorpy/cluster.py +51 -21
- phasorpy/color.py +11 -7
- phasorpy/component.py +707 -0
- phasorpy/{cursors.py → cursor.py} +31 -33
- phasorpy/datasets.py +117 -7
- phasorpy/experimental.py +310 -0
- phasorpy/io/__init__.py +138 -0
- phasorpy/io/_flimlabs.py +360 -0
- phasorpy/io/_leica.py +331 -0
- phasorpy/io/_ometiff.py +444 -0
- phasorpy/io/_other.py +890 -0
- phasorpy/io/_simfcs.py +652 -0
- phasorpy/lifetime.py +2058 -0
- phasorpy/phasor.py +184 -1754
- phasorpy/plot/__init__.py +27 -0
- phasorpy/plot/_functions.py +723 -0
- phasorpy/plot/_lifetime_plots.py +563 -0
- phasorpy/plot/_phasorplot.py +1507 -0
- phasorpy/plot/_phasorplot_fret.py +561 -0
- phasorpy/utils.py +89 -290
- {phasorpy-0.5.dist-info → phasorpy-0.7.dist-info}/METADATA +3 -3
- phasorpy-0.7.dist-info/RECORD +35 -0
- {phasorpy-0.5.dist-info → phasorpy-0.7.dist-info}/WHEEL +1 -1
- phasorpy/_io.py +0 -2655
- phasorpy/components.py +0 -313
- phasorpy/io.py +0 -9
- phasorpy/plot.py +0 -2318
- phasorpy/version.py +0 -80
- phasorpy-0.5.dist-info/RECORD +0 -26
- {phasorpy-0.5.dist-info → phasorpy-0.7.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.5.dist-info → phasorpy-0.7.dist-info}/licenses/LICENSE.txt +0 -0
- {phasorpy-0.5.dist-info → phasorpy-0.7.dist-info}/top_level.txt +0 -0
phasorpy/io/_leica.py
ADDED
@@ -0,0 +1,331 @@
|
|
1
|
+
"""Read Leica image file formats."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
__all__ = ['lifetime_from_lif', 'phasor_from_lif', 'signal_from_lif']
|
6
|
+
|
7
|
+
from typing import TYPE_CHECKING
|
8
|
+
|
9
|
+
from .._utils import xarray_metadata
|
10
|
+
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from .._typing import Any, DataArray, Literal, NDArray, PathLike
|
13
|
+
|
14
|
+
import numpy
|
15
|
+
|
16
|
+
|
17
|
+
def phasor_from_lif(
|
18
|
+
filename: str | PathLike[Any],
|
19
|
+
/,
|
20
|
+
image: str | None = None,
|
21
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], dict[str, Any]]:
|
22
|
+
"""Return phasor coordinates and metadata from Leica image file.
|
23
|
+
|
24
|
+
Leica image files may contain uncalibrated phasor coordinate images and
|
25
|
+
metadata from the analysis of FLIM measurements.
|
26
|
+
|
27
|
+
Parameters
|
28
|
+
----------
|
29
|
+
filename : str or Path
|
30
|
+
Name of Leica image file to read.
|
31
|
+
image : str, optional
|
32
|
+
Name of parent image containing phasor coordinates.
|
33
|
+
|
34
|
+
Returns
|
35
|
+
-------
|
36
|
+
mean : ndarray
|
37
|
+
Average intensity image.
|
38
|
+
real : ndarray
|
39
|
+
Image of real component of phasor coordinates.
|
40
|
+
imag : ndarray
|
41
|
+
Image of imaginary component of phasor coordinates.
|
42
|
+
attrs : dict
|
43
|
+
Select metadata:
|
44
|
+
|
45
|
+
- ``'dims'`` (tuple of str):
|
46
|
+
:ref:`Axes codes <axes>` for `mean` image dimensions.
|
47
|
+
- ``'frequency'`` (float):
|
48
|
+
Fundamental frequency of time-resolved phasor coordinates in MHz.
|
49
|
+
May not be present in all files.
|
50
|
+
- ``'flim_rawdata'`` (dict):
|
51
|
+
Settings from SingleMoleculeDetection/RawData XML element.
|
52
|
+
- ``'flim_phasor_channels'`` (list of dict):
|
53
|
+
Settings from SingleMoleculeDetection/.../PhasorData/Channels XML
|
54
|
+
elements.
|
55
|
+
|
56
|
+
Raises
|
57
|
+
------
|
58
|
+
liffile.LifFileError
|
59
|
+
File is not a Leica image file.
|
60
|
+
ValueError
|
61
|
+
File or `image` do not contain phasor coordinates and metadata.
|
62
|
+
|
63
|
+
Notes
|
64
|
+
-----
|
65
|
+
The implementation is based on the
|
66
|
+
`liffile <https://github.com/cgohlke/liffile/>`__ library.
|
67
|
+
|
68
|
+
Examples
|
69
|
+
--------
|
70
|
+
>>> mean, real, imag, attrs = phasor_from_lif(fetch('FLIM_testdata.lif'))
|
71
|
+
>>> real.shape
|
72
|
+
(1024, 1024)
|
73
|
+
>>> attrs['dims']
|
74
|
+
('Y', 'X')
|
75
|
+
>>> attrs['frequency']
|
76
|
+
19.505
|
77
|
+
|
78
|
+
"""
|
79
|
+
# TODO: read harmonic from XML if possible
|
80
|
+
# TODO: get calibration settings from XML metadata, lifetime, or
|
81
|
+
# phasor plot images
|
82
|
+
import liffile
|
83
|
+
|
84
|
+
image = '' if image is None else f'.*{image}.*/'
|
85
|
+
samples = 1
|
86
|
+
|
87
|
+
with liffile.LifFile(filename) as lif:
|
88
|
+
try:
|
89
|
+
im = lif.images[image + 'Phasor Intensity$']
|
90
|
+
dims = im.dims
|
91
|
+
coords = im.coords
|
92
|
+
# meta = image.attrs
|
93
|
+
mean = im.asarray().astype(numpy.float32)
|
94
|
+
real = lif.images[image + 'Phasor Real$'].asarray()
|
95
|
+
imag = lif.images[image + 'Phasor Imaginary$'].asarray()
|
96
|
+
# mask = lif.images[image + 'Phasor Mask$'].asarray()
|
97
|
+
except Exception as exc:
|
98
|
+
raise ValueError(
|
99
|
+
f'{lif.filename!r} does not contain Phasor images'
|
100
|
+
) from exc
|
101
|
+
|
102
|
+
attrs: dict[str, Any] = {'dims': dims, 'coords': coords}
|
103
|
+
flim = im.parent_image
|
104
|
+
if flim is not None and isinstance(flim, liffile.LifFlimImage):
|
105
|
+
xml = flim.parent.xml_element
|
106
|
+
frequency = xml.find('.//Dataset/RawData/LaserPulseFrequency')
|
107
|
+
if frequency is not None and frequency.text is not None:
|
108
|
+
attrs['frequency'] = float(frequency.text) * 1e-6
|
109
|
+
clock_period = xml.find('.//Dataset/RawData/ClockPeriod')
|
110
|
+
if clock_period is not None and clock_period.text is not None:
|
111
|
+
tmp = float(clock_period.text) * float(frequency.text)
|
112
|
+
samples = int(round(1.0 / tmp))
|
113
|
+
attrs['samples'] = samples
|
114
|
+
channels = []
|
115
|
+
for channel in xml.findall(
|
116
|
+
'.//Dataset/FlimData/PhasorData/Channels'
|
117
|
+
):
|
118
|
+
ch = liffile.xml2dict(channel)['Channels']
|
119
|
+
ch.pop('PhasorPlotShapes', None)
|
120
|
+
channels.append(ch)
|
121
|
+
attrs['flim_phasor_channels'] = channels
|
122
|
+
attrs['flim_rawdata'] = flim.attrs.get('RawData', {})
|
123
|
+
|
124
|
+
if samples > 1:
|
125
|
+
mean /= samples
|
126
|
+
return (
|
127
|
+
mean,
|
128
|
+
real.astype(numpy.float32),
|
129
|
+
imag.astype(numpy.float32),
|
130
|
+
attrs,
|
131
|
+
)
|
132
|
+
|
133
|
+
|
134
|
+
def lifetime_from_lif(
|
135
|
+
filename: str | PathLike[Any],
|
136
|
+
/,
|
137
|
+
image: str | None = None,
|
138
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], dict[str, Any]]:
|
139
|
+
"""Return lifetime image and metadata from Leica image file.
|
140
|
+
|
141
|
+
Leica image files may contain fluorescence lifetime images and metadata
|
142
|
+
from the analysis of FLIM measurements.
|
143
|
+
The lifetimes are average photon arrival times ("Fast FLIM") according to
|
144
|
+
the LAS X FLIM/FCS documentation.
|
145
|
+
|
146
|
+
Parameters
|
147
|
+
----------
|
148
|
+
filename : str or Path
|
149
|
+
Name of Leica image file to read.
|
150
|
+
image : str, optional
|
151
|
+
Name of parent image containing lifetime image.
|
152
|
+
|
153
|
+
Returns
|
154
|
+
-------
|
155
|
+
lifetime : ndarray
|
156
|
+
Fast FLIM lifetime image in ns.
|
157
|
+
intensity : ndarray
|
158
|
+
Fluorescence intensity image.
|
159
|
+
stddev : ndarray
|
160
|
+
Standard deviation of fluorescence lifetimes in ns.
|
161
|
+
attrs : dict
|
162
|
+
Select metadata:
|
163
|
+
|
164
|
+
- ``'dims'`` (tuple of str):
|
165
|
+
:ref:`Axes codes <axes>` for `intensity` image dimensions.
|
166
|
+
- ``'frequency'`` (float):
|
167
|
+
Fundamental frequency of lifetimes in MHz.
|
168
|
+
May not be present in all files.
|
169
|
+
- ``'samples'`` (int):
|
170
|
+
Number of bins in TCSPC histogram. May not be present in all files.
|
171
|
+
- ``'flim_rawdata'`` (dict):
|
172
|
+
Settings from SingleMoleculeDetection/RawData XML element.
|
173
|
+
|
174
|
+
Raises
|
175
|
+
------
|
176
|
+
liffile.LifFileError
|
177
|
+
File is not a Leica image file.
|
178
|
+
ValueError
|
179
|
+
File or `image` does not contain lifetime coordinates and metadata.
|
180
|
+
|
181
|
+
Notes
|
182
|
+
-----
|
183
|
+
The implementation is based on the
|
184
|
+
`liffile <https://github.com/cgohlke/liffile/>`__ library.
|
185
|
+
|
186
|
+
Examples
|
187
|
+
--------
|
188
|
+
>>> lifetime, intensity, stddev, attrs = lifetime_from_lif(
|
189
|
+
... fetch('FLIM_testdata.lif')
|
190
|
+
... )
|
191
|
+
>>> lifetime.shape
|
192
|
+
(1024, 1024)
|
193
|
+
>>> attrs['dims']
|
194
|
+
('Y', 'X')
|
195
|
+
>>> attrs['frequency']
|
196
|
+
19.505
|
197
|
+
|
198
|
+
"""
|
199
|
+
import liffile
|
200
|
+
|
201
|
+
image = '' if image is None else f'.*{image}.*/'
|
202
|
+
|
203
|
+
with liffile.LifFile(filename) as lif:
|
204
|
+
try:
|
205
|
+
im = lif.images[image + 'Intensity$']
|
206
|
+
dims = im.dims
|
207
|
+
coords = im.coords
|
208
|
+
# meta = im.attrs
|
209
|
+
intensity = im.asarray()
|
210
|
+
lifetime = lif.images[image + 'Fast Flim$'].asarray()
|
211
|
+
stddev = lif.images[image + 'Standard Deviation$'].asarray()
|
212
|
+
except Exception as exc:
|
213
|
+
raise ValueError(
|
214
|
+
f'{lif.filename!r} does not contain lifetime images'
|
215
|
+
) from exc
|
216
|
+
|
217
|
+
attrs: dict[str, Any] = {'dims': dims, 'coords': coords}
|
218
|
+
flim = im.parent_image
|
219
|
+
if flim is not None and isinstance(flim, liffile.LifFlimImage):
|
220
|
+
xml = flim.parent.xml_element
|
221
|
+
frequency = xml.find('.//Dataset/RawData/LaserPulseFrequency')
|
222
|
+
if frequency is not None and frequency.text is not None:
|
223
|
+
attrs['frequency'] = float(frequency.text) * 1e-6
|
224
|
+
clock_period = xml.find('.//Dataset/RawData/ClockPeriod')
|
225
|
+
if clock_period is not None and clock_period.text is not None:
|
226
|
+
tmp = float(clock_period.text) * float(frequency.text)
|
227
|
+
samples = int(round(1.0 / tmp))
|
228
|
+
attrs['samples'] = samples
|
229
|
+
attrs['flim_rawdata'] = flim.attrs.get('RawData', {})
|
230
|
+
|
231
|
+
return (
|
232
|
+
lifetime.astype(numpy.float32),
|
233
|
+
intensity.astype(numpy.float32),
|
234
|
+
stddev.astype(numpy.float32),
|
235
|
+
attrs,
|
236
|
+
)
|
237
|
+
|
238
|
+
|
239
|
+
def signal_from_lif(
|
240
|
+
filename: str | PathLike[Any],
|
241
|
+
/,
|
242
|
+
*,
|
243
|
+
image: int | str | None = None,
|
244
|
+
dim: Literal['λ', 'Λ'] | str = 'λ',
|
245
|
+
) -> DataArray:
|
246
|
+
"""Return hyperspectral image and metadata from Leica image file.
|
247
|
+
|
248
|
+
Leica image files may contain hyperspectral images and metadata from laser
|
249
|
+
scanning microscopy measurements.
|
250
|
+
|
251
|
+
Parameters
|
252
|
+
----------
|
253
|
+
filename : str or Path
|
254
|
+
Name of Leica image file to read.
|
255
|
+
image : str or int, optional
|
256
|
+
Index or regex pattern of image to return.
|
257
|
+
By default, return the first image containing hyperspectral data.
|
258
|
+
dim : str or None
|
259
|
+
Character code of hyperspectral dimension.
|
260
|
+
Either ``'λ'`` for emission (default) or ``'Λ'`` for excitation.
|
261
|
+
|
262
|
+
Returns
|
263
|
+
-------
|
264
|
+
xarray.DataArray
|
265
|
+
Hyperspectral image data.
|
266
|
+
|
267
|
+
- ``coords['C']``: wavelengths in nm.
|
268
|
+
- ``coords['T']``: time coordinates in s, if any.
|
269
|
+
|
270
|
+
Raises
|
271
|
+
------
|
272
|
+
liffile.LifFileError
|
273
|
+
File is not a Leica image file.
|
274
|
+
ValueError
|
275
|
+
File is not a Leica image file or does not contain hyperspectral image.
|
276
|
+
|
277
|
+
Notes
|
278
|
+
-----
|
279
|
+
The implementation is based on the
|
280
|
+
`liffile <https://github.com/cgohlke/liffile/>`__ library.
|
281
|
+
|
282
|
+
Reading of TCSPC histograms from FLIM measurements is not supported
|
283
|
+
because the compression scheme is patent-pending.
|
284
|
+
|
285
|
+
Examples
|
286
|
+
--------
|
287
|
+
>>> signal = signal_from_lif('ScanModesExamples.lif') # doctest: +SKIP
|
288
|
+
>>> signal.values # doctest: +SKIP
|
289
|
+
array(...)
|
290
|
+
>>> signal.shape # doctest: +SKIP
|
291
|
+
(9, 128, 128)
|
292
|
+
>>> signal.dims # doctest: +SKIP
|
293
|
+
('C', 'Y', 'X')
|
294
|
+
>>> signal.coords['C'].data # doctest: +SKIP
|
295
|
+
array([560, 580, 600, ..., 680, 700, 720])
|
296
|
+
|
297
|
+
"""
|
298
|
+
import liffile
|
299
|
+
|
300
|
+
with liffile.LifFile(filename) as lif:
|
301
|
+
if image is None:
|
302
|
+
# find image with excitation or emission dimension
|
303
|
+
for im in lif.images:
|
304
|
+
if dim in im.dims:
|
305
|
+
break
|
306
|
+
else:
|
307
|
+
raise ValueError(
|
308
|
+
f'{lif.filename!r} does not contain hyperspectral image'
|
309
|
+
)
|
310
|
+
else:
|
311
|
+
im = lif.images[image]
|
312
|
+
|
313
|
+
if dim not in im.dims or im.sizes[dim] < 4:
|
314
|
+
raise ValueError(f'{im!r} does not contain spectral dimension')
|
315
|
+
if 'C' in im.dims:
|
316
|
+
raise ValueError(
|
317
|
+
'hyperspectral image must not contain channel axis'
|
318
|
+
)
|
319
|
+
|
320
|
+
data = im.asarray()
|
321
|
+
coords: dict[str, Any] = {
|
322
|
+
('C' if k == dim else k): (v * 1e9 if k == dim else v)
|
323
|
+
for (k, v) in im.coords.items()
|
324
|
+
}
|
325
|
+
dims = tuple(('C' if d == dim else d) for d in im.dims)
|
326
|
+
|
327
|
+
metadata = xarray_metadata(dims, im.shape, filename, **coords)
|
328
|
+
|
329
|
+
from xarray import DataArray
|
330
|
+
|
331
|
+
return DataArray(data, **metadata)
|