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