phasorpy 0.3__cp313-cp313-win_amd64.whl → 0.4__cp313-cp313-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/io.py CHANGED
@@ -1,1811 +1,5 @@
1
- """Read and write time-resolved and hyperspectral image file formats.
1
+ # phasorpy.io proxy module
2
+ # The VSCode debugger cannot to step into or break inside modules named io.py
2
3
 
3
- The ``phasorpy.io`` module provides functions to:
4
-
5
- - read and write phasor coordinate images in OME-TIFF format, which can be
6
- imported in Bio-Formats and Fiji:
7
-
8
- - :py:func:`phasor_to_ometiff`
9
- - :py:func:`phasor_from_ometiff`
10
-
11
- - read and write phasor coordinate images in SimFCS referenced R64 format:
12
-
13
- - :py:func:`phasor_to_simfcs_referenced`
14
- - :py:func:`phasor_from_simfcs_referenced`
15
-
16
- - read time-resolved and hyperspectral image data and metadata (as relevant
17
- to phasor analysis) from many file formats used in bio-imaging:
18
-
19
- - :py:func:`read_imspector_tiff` - ImSpector FLIM TIFF
20
- - :py:func:`read_lsm` - Zeiss LSM
21
- - :py:func:`read_ifli` - ISS IFLI
22
- - :py:func:`read_sdt` - Becker & Hickl SDT
23
- - :py:func:`read_ptu` - PicoQuant PTU
24
- - :py:func:`read_fbd` - FLIMbox FBD
25
- - :py:func:`read_flif` - FlimFast FLIF
26
- - :py:func:`read_b64` - SimFCS B64
27
- - :py:func:`read_z64` - SimFCS Z64
28
- - :py:func:`read_bhz` - SimFCS BHZ
29
- - :py:func:`read_bh` - SimFCS B&H
30
-
31
- Support for other file formats is being considered:
32
-
33
- - OME-TIFF
34
- - Zeiss CZI
35
- - Leica LIF
36
- - Nikon ND2
37
- - Olympus OIB/OIF
38
- - Olympus OIR
39
-
40
- The functions are implemented as minimal wrappers around specialized
41
- third-party file reader libraries, currently
42
- `tifffile <https://github.com/cgohlke/tifffile>`_,
43
- `ptufile <https://github.com/cgohlke/ptufile>`_,
44
- `sdtfile <https://github.com/cgohlke/sdtfile>`_, and
45
- `lfdfiles <https://github.com/cgohlke/lfdfiles>`_.
46
- For advanced or unsupported use cases, consider using these libraries directly.
47
-
48
- The read functions typically have the following signature::
49
-
50
- read_ext(
51
- filename: str | PathLike,
52
- /,
53
- **kwargs
54
- ): -> xarray.DataArray
55
-
56
- where ``ext`` indicates the file format and ``kwargs`` are optional arguments
57
- passed to the underlying file reader library or used to select which data is
58
- returned. The returned `xarray.DataArray
59
- <https://docs.xarray.dev/en/stable/user-guide/data-structures.html>`_
60
- contains an n-dimensional array with labeled coordinates, dimensions, and
61
- attributes:
62
-
63
- - ``data`` or ``values`` (*array_like*)
64
-
65
- Numpy array or array-like holding the array's values.
66
-
67
- - ``dims`` (*tuple of str*)
68
-
69
- :ref:`Axes character codes <axes>` for each dimension in ``data``.
70
- For example, ``('T', 'C', 'Y', 'X')`` defines the dimension order in a
71
- 4-dimensional array of a time-series of multi-channel images.
72
-
73
- - ``coords`` (*dict_like[str, array_like]*)
74
-
75
- Coordinate arrays labelling each point in the data array.
76
- The keys are :ref:`axes character codes <axes>`.
77
- Values are 1-dimensional arrays of numbers or strings.
78
- For example, ``coords['C']`` could be an array of emission wavelengths.
79
-
80
- - ``attrs`` (*dict[str, Any]*)
81
-
82
- Arbitrary metadata such as measurement or calibration parameters required to
83
- interpret the data values.
84
- For example, the laser repetition frequency of a time-resolved measurement.
85
-
86
- .. _axes:
87
-
88
- Axes character codes from the OME model and tifffile library are used as
89
- ``dims`` items and ``coords`` keys:
90
-
91
- - ``'X'`` : width (OME)
92
- - ``'Y'`` : height (OME)
93
- - ``'Z'`` : depth (OME)
94
- - ``'S'`` : sample (color components or phasor coordinates)
95
- - ``'I'`` : sequence (of images, frames, or planes)
96
- - ``'T'`` : time (OME)
97
- - ``'C'`` : channel (OME. Acquisition path or emission wavelength)
98
- - ``'A'`` : angle (OME)
99
- - ``'P'`` : phase (OME. In LSM, ``'P'`` maps to position)
100
- - ``'R'`` : tile (OME. Region, position, or mosaic)
101
- - ``'H'`` : lifetime histogram (OME)
102
- - ``'E'`` : lambda (OME. Excitation wavelength)
103
- - ``'F'`` : frequency (ISS)
104
- - ``'Q'`` : other (OME. Harmonics in PhasorPy TIFF)
105
- - ``'L'`` : exposure (FluoView)
106
- - ``'V'`` : event (FluoView)
107
- - ``'M'`` : mosaic (LSM 6)
108
- - ``'J'`` : column (NDTiff)
109
- - ``'K'`` : row (NDTiff)
110
-
111
- """
112
-
113
- from __future__ import annotations
114
-
115
- __all__ = [
116
- 'phasor_from_ometiff',
117
- 'phasor_from_simfcs_referenced',
118
- 'phasor_to_ometiff',
119
- 'phasor_to_simfcs_referenced',
120
- 'read_b64',
121
- 'read_bh',
122
- 'read_bhz',
123
- # 'read_czi',
124
- 'read_fbd',
125
- 'read_flif',
126
- 'read_ifli',
127
- 'read_imspector_tiff',
128
- # 'read_lif',
129
- 'read_lsm',
130
- # 'read_nd2',
131
- # 'read_oif',
132
- # 'read_oir',
133
- # 'read_ometiff',
134
- 'read_ptu',
135
- 'read_sdt',
136
- 'read_z64',
137
- '_squeeze_axes',
138
- ]
139
-
140
- import logging
141
- import os
142
- import re
143
- import struct
144
- import zlib
145
- from typing import TYPE_CHECKING
146
-
147
- from ._utils import chunk_iter, parse_harmonic
148
- from .phasor import phasor_from_polar, phasor_to_polar
149
-
150
- if TYPE_CHECKING:
151
- from ._typing import (
152
- Any,
153
- ArrayLike,
154
- DataArray,
155
- DTypeLike,
156
- EllipsisType,
157
- Literal,
158
- NDArray,
159
- PathLike,
160
- Sequence,
161
- )
162
-
163
- import numpy
164
-
165
- logger = logging.getLogger(__name__)
166
-
167
-
168
- def phasor_to_ometiff(
169
- filename: str | PathLike[Any],
170
- mean: ArrayLike,
171
- real: ArrayLike,
172
- imag: ArrayLike,
173
- /,
174
- *,
175
- frequency: float | None = None,
176
- harmonic: int | Sequence[int] | None = None,
177
- axes: str | None = None,
178
- dtype: DTypeLike | None = None,
179
- description: str | None = None,
180
- **kwargs: Any,
181
- ) -> None:
182
- """Write phasor coordinate images and metadata to OME-TIFF file.
183
-
184
- The OME-TIFF format is compatible with Bio-Formats and Fiji.
185
-
186
- By default, write phasor coordinates as single precision floating point
187
- values to separate image series.
188
- Write images larger than (1024, 1024) as (256, 256) tiles, datasets
189
- larger than 2 GB as BigTIFF, and datasets larger than 8 KB zlib-compressed.
190
-
191
- This file format is experimental and might be incompatible with future
192
- versions of this library. It is intended for temporarily exchanging
193
- phasor coordinates with other software, not as a long-term storage
194
- solution.
195
-
196
- Parameters
197
- ----------
198
- filename : str or Path
199
- Name of OME-TIFF file to write.
200
- mean : array_like
201
- Average intensity image. Write to image series named 'Phasor mean'.
202
- real : array_like
203
- Image of real component of phasor coordinates.
204
- Multiple harmonics, if any, must be in the first dimension.
205
- Write to image series named 'Phasor real'.
206
- imag : array_like
207
- Image of imaginary component of phasor coordinates.
208
- Multiple harmonics, if any, must be in the first dimension.
209
- Write to image series named 'Phasor imag'.
210
- frequency : float, optional
211
- Fundamental frequency of time-resolved phasor coordinates.
212
- Write to image series named 'Phasor frequency'.
213
- harmonic : int or sequence of int, optional
214
- Harmonics present in the first dimension of `real` and `imag`, if any.
215
- Write to image series named 'Phasor harmonic'.
216
- Only needed if harmonics are not starting at and increasing by one.
217
- axes : str, optional
218
- Character codes for `mean` image dimensions.
219
- By default, the last dimensions are assumed to be 'TZCYX'.
220
- If harmonics are present in `real` and `imag`, an "other" (``Q``)
221
- dimension is prepended to axes for those arrays.
222
- Refer to the OME-TIFF model for allowed axes and their order.
223
- dtype : dtype-like, optional
224
- Floating point data type used to store phasor coordinates.
225
- The default is ``float32``, which has 6 digits of precision
226
- and maximizes compatibility with other software.
227
- description : str, optional
228
- Plain-text description of dataset. Write as OME dataset description.
229
- **kwargs
230
- Additional arguments passed to :py:class:`tifffile.TiffWriter` and
231
- :py:meth:`tifffile.TiffWriter.write`.
232
- For example, ``compression=None`` writes image data uncompressed.
233
-
234
- See Also
235
- --------
236
- phasorpy.io.phasor_from_ometiff
237
-
238
- Notes
239
- -----
240
- Scalar or one-dimensional phasor coordinate arrays are written as images.
241
-
242
- The OME-TIFF format is specified in the
243
- `OME Data Model and File Formats Documentation
244
- <https://ome-model.readthedocs.io/>`_.
245
-
246
- The `6D, 7D and 8D storage
247
- <https://ome-model.readthedocs.io/en/latest/developers/6d-7d-and-8d-storage.html>`_
248
- extension is used to store multi-harmonic phasor coordinates.
249
- The modulo type for the first, harmonic dimension is "other".
250
-
251
- Examples
252
- --------
253
- >>> mean, real, imag = numpy.random.rand(3, 32, 32, 32)
254
- >>> phasor_to_ometiff(
255
- ... '_phasorpy.ome.tif', mean, real, imag, axes='ZYX', frequency=80.0
256
- ... )
257
-
258
- """
259
- import tifffile
260
-
261
- from .version import __version__
262
-
263
- if dtype is None:
264
- dtype = numpy.float32
265
- dtype = numpy.dtype(dtype)
266
- if dtype.kind != 'f':
267
- raise ValueError(f'{dtype=} not a floating point type')
268
-
269
- mean = numpy.asarray(mean, dtype)
270
- real = numpy.asarray(real, dtype)
271
- imag = numpy.asarray(imag, dtype)
272
- datasize = mean.nbytes + real.nbytes + imag.nbytes
273
-
274
- if real.shape != imag.shape:
275
- raise ValueError(f'{real.shape=} != {imag.shape=}')
276
- if mean.shape != real.shape[-mean.ndim :]:
277
- raise ValueError(f'{mean.shape=} != {real.shape[-mean.ndim:]=}')
278
- has_harmonic_dim = real.ndim == mean.ndim + 1
279
- if mean.ndim == real.ndim or real.ndim == 0:
280
- nharmonic = 1
281
- else:
282
- nharmonic = real.shape[0]
283
-
284
- if mean.ndim < 2:
285
- # not an image
286
- mean = mean.reshape(1, -1)
287
- if has_harmonic_dim:
288
- real = real.reshape(real.shape[0], 1, -1)
289
- imag = imag.reshape(imag.shape[0], 1, -1)
290
- else:
291
- real = real.reshape(1, -1)
292
- imag = imag.reshape(1, -1)
293
-
294
- if harmonic is not None:
295
- harmonic, _ = parse_harmonic(harmonic)
296
- if len(harmonic) != nharmonic:
297
- raise ValueError('invalid harmonic')
298
-
299
- if frequency is not None:
300
- frequency_array = numpy.atleast_2d(frequency).astype(numpy.float64)
301
- if frequency_array.size > 1:
302
- raise ValueError('frequency must be scalar')
303
-
304
- if axes is None:
305
- axes = 'TZCYX'[-mean.ndim :]
306
- else:
307
- axes = ''.join(tuple(axes)) # accept dims tuple and str
308
- if len(axes) != mean.ndim:
309
- raise ValueError(f'{axes=} does not match {mean.ndim=}')
310
- axes_phasor = axes if mean.ndim == real.ndim else 'Q' + axes
311
-
312
- if 'photometric' not in kwargs:
313
- kwargs['photometric'] = 'minisblack'
314
- if 'compression' not in kwargs and datasize > 8192:
315
- kwargs['compression'] = 'zlib'
316
- if 'tile' not in kwargs and 'rowsperstrip' not in kwargs:
317
- if (
318
- axes.endswith('YX')
319
- and mean.shape[-1] > 1024
320
- and mean.shape[-2] > 1024
321
- ):
322
- kwargs['tile'] = (256, 256)
323
-
324
- mode = kwargs.pop('mode', None)
325
- bigtiff = kwargs.pop('bigtiff', None)
326
- if bigtiff is None:
327
- bigtiff = datasize > 2**31
328
-
329
- metadata = kwargs.pop('metadata', {})
330
- if 'Creator' not in metadata:
331
- metadata['Creator'] = f'PhasorPy {__version__}'
332
-
333
- dataset = metadata.pop('Dataset', {})
334
- if 'Name' not in dataset:
335
- dataset['Name'] = 'Phasor'
336
- if description:
337
- dataset['Description'] = description
338
- metadata['Dataset'] = dataset
339
-
340
- if has_harmonic_dim:
341
- metadata['TypeDescription'] = {'Q': 'Phasor harmonics'}
342
-
343
- with tifffile.TiffWriter(
344
- filename, bigtiff=bigtiff, mode=mode, ome=True
345
- ) as tif:
346
- metadata['Name'] = 'Phasor mean'
347
- metadata['axes'] = axes
348
- tif.write(mean, metadata=metadata, **kwargs)
349
- del metadata['Dataset']
350
-
351
- metadata['Name'] = 'Phasor real'
352
- metadata['axes'] = axes_phasor
353
- tif.write(real, metadata=metadata, **kwargs)
354
-
355
- metadata['Name'] = 'Phasor imag'
356
- tif.write(imag, metadata=metadata, **kwargs)
357
-
358
- if frequency is not None:
359
- tif.write(frequency_array, metadata={'Name': 'Phasor frequency'})
360
-
361
- if harmonic is not None:
362
- tif.write(
363
- numpy.atleast_2d(harmonic).astype(numpy.uint32),
364
- metadata={'Name': 'Phasor harmonic'},
365
- )
366
-
367
-
368
- def phasor_from_ometiff(
369
- filename: str | PathLike[Any],
370
- /,
371
- *,
372
- harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
373
- ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], dict[str, Any]]:
374
- """Return phasor images and metadata from OME-TIFF written by PhasorPy.
375
-
376
- Parameters
377
- ----------
378
- filename : str or Path
379
- Name of OME-TIFF file to read.
380
- harmonic : int, sequence of int, or 'all', optional
381
- Harmonic(s) to return from file.
382
- If None (default), return the first harmonic stored in the file.
383
- If `'all'`, return all harmonics as stored in file.
384
- If a list, the first axes of the returned `real` and `imag` arrays
385
- contain specified harmonic(s).
386
- If an integer, the returned `real` and `imag` arrays are single
387
- harmonic and have the same shape as `mean`.
388
-
389
- Returns
390
- -------
391
- mean : ndarray
392
- Average intensity image.
393
- real : ndarray
394
- Image of real component of phasor coordinates.
395
- imag : ndarray
396
- Image of imaginary component of phasor coordinates.
397
- attrs : dict
398
- Select metadata:
399
-
400
- - ``'axes'`` (str):
401
- Character codes for `mean` image dimensions.
402
- - ``'harmonic'`` (int or list of int):
403
- Harmonic(s) present in `real` and `imag`.
404
- If a scalar, `real` and `imag` are single harmonic and contain no
405
- harmonic axes.
406
- If a list, `real` and `imag` contain one or more harmonics in the
407
- first axis.
408
- - ``'frequency'`` (float, optional):
409
- Fundamental frequency of time-resolved phasor coordinates.
410
- - ``'description'`` (str, optional):
411
- OME dataset plain-text description.
412
-
413
- Raises
414
- ------
415
- tifffile.TiffFileError
416
- File is not a TIFF file.
417
- ValueError
418
- File is not an OME-TIFF containing phasor coordinates.
419
- IndexError
420
- Requested harmonic is not found in file.
421
-
422
- See Also
423
- --------
424
- phasorpy.io.phasor_to_ometiff
425
-
426
- Notes
427
- -----
428
- Scalar or one-dimensional phasor coordinates stored in the file are
429
- returned as two-dimensional images (three-dimensional if multiple
430
- harmonics are present).
431
-
432
- Examples
433
- --------
434
- >>> mean, real, imag = numpy.random.rand(3, 32, 32, 32)
435
- >>> phasor_to_ometiff(
436
- ... '_phasorpy.ome.tif', mean, real, imag, axes='ZYX', frequency=80.0
437
- ... )
438
- >>> mean, real, imag, attrs = phasor_from_ometiff('_phasorpy.ome.tif')
439
- >>> mean
440
- array(...)
441
- >>> mean.dtype
442
- dtype('float32')
443
- >>> mean.shape
444
- (32, 32, 32)
445
- >>> attrs['axes']
446
- 'ZYX'
447
- >>> attrs['frequency']
448
- 80.0
449
- >>> attrs['harmonic']
450
- 1
451
-
452
- """
453
- import tifffile
454
-
455
- name = os.path.basename(filename)
456
-
457
- with tifffile.TiffFile(filename) as tif:
458
- if (
459
- not tif.is_ome
460
- or len(tif.series) < 3
461
- or tif.series[0].name != 'Phasor mean'
462
- or tif.series[1].name != 'Phasor real'
463
- or tif.series[2].name != 'Phasor imag'
464
- ):
465
- raise ValueError(
466
- f'{name!r} is not an OME-TIFF containing phasor images'
467
- )
468
-
469
- attrs: dict[str, Any] = {'axes': tif.series[0].axes}
470
-
471
- # TODO: read coords from OME-XML
472
- ome_xml = tif.ome_metadata
473
- assert ome_xml is not None
474
-
475
- # TODO: parse OME-XML
476
- match = re.search(
477
- r'><Description>(.*)</Description><',
478
- ome_xml,
479
- re.MULTILINE | re.DOTALL,
480
- )
481
- if match is not None:
482
- attrs['description'] = (
483
- match.group(1)
484
- .replace('&amp;', '&')
485
- .replace('&gt;', '>')
486
- .replace('&lt;', '<')
487
- )
488
-
489
- has_harmonic_dim = tif.series[1].ndim > tif.series[0].ndim
490
- nharmonics = tif.series[1].shape[0] if has_harmonic_dim else 1
491
- harmonic_max = nharmonics
492
- for i in (3, 4):
493
- if len(tif.series) < i + 1:
494
- break
495
- series = tif.series[i]
496
- data = series.asarray().squeeze()
497
- if series.name == 'Phasor frequency':
498
- attrs['frequency'] = float(data.item(0))
499
- elif series.name == 'Phasor harmonic':
500
- if not has_harmonic_dim and data.size == 1:
501
- attrs['harmonic'] = int(data.item(0))
502
- harmonic_max = attrs['harmonic']
503
- elif has_harmonic_dim and data.size == nharmonics:
504
- attrs['harmonic'] = data.tolist()
505
- harmonic_max = max(attrs['harmonic'])
506
- else:
507
- logger.warning(
508
- f'harmonic={data} does not match phasor '
509
- f'shape={tif.series[1].shape}'
510
- )
511
-
512
- if 'harmonic' not in attrs:
513
- if has_harmonic_dim:
514
- attrs['harmonic'] = list(range(1, nharmonics + 1))
515
- else:
516
- attrs['harmonic'] = 1
517
- harmonic_stored = attrs['harmonic']
518
-
519
- mean = tif.series[0].asarray()
520
- if harmonic is None:
521
- # first harmonic in file
522
- if isinstance(harmonic_stored, list):
523
- attrs['harmonic'] = harmonic_stored[0]
524
- else:
525
- attrs['harmonic'] = harmonic_stored
526
- real = tif.series[1].asarray()
527
- if has_harmonic_dim:
528
- real = real[0].copy()
529
- imag = tif.series[2].asarray()
530
- if has_harmonic_dim:
531
- imag = imag[0].copy()
532
- elif isinstance(harmonic, str) and harmonic == 'all':
533
- # all harmonics as stored in file
534
- real = tif.series[1].asarray()
535
- imag = tif.series[2].asarray()
536
- else:
537
- # specified harmonics
538
- harmonic, keepdims = parse_harmonic(harmonic, harmonic_max)
539
- try:
540
- if isinstance(harmonic_stored, list):
541
- index = [harmonic_stored.index(h) for h in harmonic]
542
- else:
543
- index = [[harmonic_stored].index(h) for h in harmonic]
544
- except ValueError as exc:
545
- raise IndexError('harmonic not found') from exc
546
-
547
- if has_harmonic_dim:
548
- if keepdims:
549
- attrs['harmonic'] = [harmonic_stored[i] for i in index]
550
- real = tif.series[1].asarray()[index].copy()
551
- imag = tif.series[2].asarray()[index].copy()
552
- else:
553
- attrs['harmonic'] = harmonic_stored[index[0]]
554
- real = tif.series[1].asarray()[index[0]].copy()
555
- imag = tif.series[2].asarray()[index[0]].copy()
556
- elif keepdims:
557
- real = tif.series[1].asarray()
558
- real = real.reshape(1, *real.shape)
559
- imag = tif.series[2].asarray()
560
- imag = imag.reshape(1, *imag.shape)
561
- attrs['harmonic'] = [harmonic_stored]
562
- else:
563
- real = tif.series[1].asarray()
564
- imag = tif.series[2].asarray()
565
-
566
- if real.shape != imag.shape:
567
- logger.warning(f'{real.shape=} != {imag.shape=}')
568
- if real.shape[-mean.ndim :] != mean.shape:
569
- logger.warning(f'{real.shape[-mean.ndim:]=} != {mean.shape=}')
570
-
571
- return mean, real, imag, attrs
572
-
573
-
574
- def phasor_to_simfcs_referenced(
575
- filename: str | PathLike[Any],
576
- mean: ArrayLike,
577
- real: ArrayLike,
578
- imag: ArrayLike,
579
- /,
580
- *,
581
- size: int | None = None,
582
- axes: str | None = None,
583
- ) -> None:
584
- """Write phasor coordinate images to SimFCS referenced R64 file(s).
585
-
586
- SimFCS referenced R64 files store square-shaped (commonly 256x256)
587
- images of the average intensity, and the calibrated phasor coordinates
588
- (encoded as phase and modulation) of two harmonics as ZIP-compressed,
589
- single precision floating point arrays.
590
- The file format does not support any metadata.
591
-
592
- Images with more than two dimensions or larger than square size are
593
- chunked to square-sized images and saved to separate files with
594
- a name pattern, for example, "filename_T099_Y256_X000.r64".
595
- Images or chunks with less than two dimensions or smaller than square size
596
- are padded with NaN values.
597
-
598
- Parameters
599
- ----------
600
- filename : str or Path
601
- Name of SimFCS referenced R64 file to write.
602
- The file extension must be ``.r64``.
603
- mean : array_like
604
- Average intensity image.
605
- real : array_like
606
- Image of real component of calibrated phasor coordinates.
607
- Multiple harmonics, if any, must be in the first dimension.
608
- Harmonics must be starting at and increasing by one.
609
- imag : array_like
610
- Image of imaginary component of calibrated phasor coordinates.
611
- Multiple harmonics, if any, must be in the first dimension.
612
- Harmonics must be starting at and increasing by one.
613
- size : int, optional
614
- Size of X and Y dimensions of square-sized images stored in file.
615
- By default, ``size = min(256, max(4, sizey, sizex))``.
616
- axes : str, optional
617
- Character codes for `mean` dimensions used to format file names.
618
-
619
- See Also
620
- --------
621
- phasorpy.io.phasor_from_simfcs_referenced
622
-
623
- Examples
624
- --------
625
- >>> mean, real, imag = numpy.random.rand(3, 32, 32)
626
- >>> phasor_to_simfcs_referenced('_phasorpy.r64', mean, real, imag)
627
-
628
- """
629
- filename, ext = os.path.splitext(filename)
630
- if ext.lower() != '.r64':
631
- raise ValueError(f'file extension {ext} != .r64')
632
-
633
- # TODO: delay conversions to numpy arrays to inner loop
634
- mean = numpy.asarray(mean, numpy.float32)
635
- phi, mod = phasor_to_polar(real, imag, dtype=numpy.float32)
636
- del real
637
- del imag
638
- phi = numpy.rad2deg(phi)
639
-
640
- if phi.shape != mod.shape:
641
- raise ValueError(f'{phi.shape=} != {mod.shape=}')
642
- if mean.shape != phi.shape[-mean.ndim :]:
643
- raise ValueError(f'{mean.shape=} != {phi.shape[-mean.ndim:]=}')
644
- if phi.ndim == mean.ndim:
645
- phi = phi.reshape(1, *phi.shape)
646
- mod = mod.reshape(1, *mod.shape)
647
- nharmonic = phi.shape[0]
648
-
649
- if mean.ndim < 2:
650
- # not an image
651
- mean = mean.reshape(1, -1)
652
- phi = phi.reshape(nharmonic, 1, -1)
653
- mod = mod.reshape(nharmonic, 1, -1)
654
-
655
- # TODO: investigate actual size and harmonics limits of SimFCS
656
- sizey, sizex = mean.shape[-2:]
657
- if size is None:
658
- size = min(256, max(4, sizey, sizex))
659
- elif not 4 <= size <= 65535:
660
- raise ValueError(f'{size=} out of range [4..65535]')
661
-
662
- harmonics_per_file = 2 # TODO: make this a parameter?
663
- chunk_shape = tuple(
664
- [max(harmonics_per_file, 2)] + ([1] * (phi.ndim - 3)) + [size, size]
665
- )
666
- multi_file = any(i / j > 1 for i, j in zip(phi.shape, chunk_shape))
667
-
668
- if axes is not None and len(axes) == phi.ndim - 1:
669
- axes = 'h' + axes
670
-
671
- chunk = numpy.empty((size, size), dtype=numpy.float32)
672
-
673
- def rawdata_append(
674
- rawdata: list[bytes], a: NDArray[Any] | None = None
675
- ) -> None:
676
- if a is None:
677
- chunk[:] = numpy.nan
678
- rawdata.append(chunk.tobytes())
679
- else:
680
- sizey, sizex = a.shape[-2:]
681
- if sizey == size and sizex == size:
682
- rawdata.append(a.tobytes())
683
- elif sizey <= size and sizex <= size:
684
- chunk[:sizey, :sizex] = a[..., :sizey, :sizex]
685
- chunk[sizey:, sizex:] = numpy.nan
686
- rawdata.append(chunk.tobytes())
687
- else:
688
- raise RuntimeError # should not be reached
689
-
690
- for index, label, _ in chunk_iter(
691
- phi.shape, chunk_shape, axes, squeeze=False, use_index=True
692
- ):
693
- rawdata = [struct.pack('I', size)]
694
- rawdata_append(rawdata, mean[index[1:]])
695
- phi_ = phi[index]
696
- mod_ = mod[index]
697
- for i in range(phi_.shape[0]):
698
- rawdata_append(rawdata, phi_[i])
699
- rawdata_append(rawdata, mod_[i])
700
- if phi_.shape[0] == 1:
701
- rawdata_append(rawdata)
702
- rawdata_append(rawdata)
703
-
704
- if not multi_file:
705
- label = ''
706
- with open(filename + label + ext, 'wb') as fh:
707
- fh.write(zlib.compress(b''.join(rawdata)))
708
-
709
-
710
- def phasor_from_simfcs_referenced(
711
- filename: str | PathLike[Any],
712
- /,
713
- *,
714
- harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
715
- ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
716
- """Return phasor coordinate images from SimFCS referenced (REF, R64) file.
717
-
718
- SimFCS referenced REF and R64 files contain phasor coordinate images
719
- (encoded as phase and modulation) for two harmonics.
720
- Phasor coordinates from lifetime-resolved signals are calibrated.
721
-
722
- Parameters
723
- ----------
724
- filename : str or Path
725
- Name of REF or R64 file to read.
726
- harmonic : int or sequence of int, optional
727
- Harmonic(s) to include in returned phasor coordinates.
728
- By default, only the first harmonic is returned.
729
-
730
- Returns
731
- -------
732
- mean : ndarray
733
- Average intensity image.
734
- real : ndarray
735
- Image of real component of phasor coordinates.
736
- Multiple harmonics, if any, are in the first axis.
737
- imag : ndarray
738
- Image of imaginary component of phasor coordinates.
739
- Multiple harmonics, if any, are in the first axis.
740
-
741
- Raises
742
- ------
743
- lfdfiles.LfdfileError
744
- File is not a SimFCS REF or R64 file.
745
-
746
- See Also
747
- --------
748
- phasorpy.io.phasor_to_simfcs_referenced
749
-
750
- Examples
751
- --------
752
- >>> phasor_to_simfcs_referenced(
753
- ... '_phasorpy.r64', *numpy.random.rand(3, 32, 32)
754
- ... )
755
- >>> mean, real, imag = phasor_from_simfcs_referenced('_phasorpy.r64')
756
- >>> mean
757
- array([[...]], dtype=float32)
758
-
759
- """
760
- import lfdfiles
761
-
762
- ext = os.path.splitext(filename)[-1].lower()
763
- if ext == '.r64':
764
- with lfdfiles.SimfcsR64(filename) as r64:
765
- data = r64.asarray()
766
- elif ext == '.ref':
767
- with lfdfiles.SimfcsRef(filename) as ref:
768
- data = ref.asarray()
769
- else:
770
- raise ValueError(f'file extension must be .ref or .r64, not {ext!r}')
771
-
772
- harmonic, keep_harmonic_dim = parse_harmonic(harmonic, data.shape[0] // 2)
773
-
774
- mean = data[0].copy()
775
- real = numpy.empty((len(harmonic),) + mean.shape, numpy.float32)
776
- imag = numpy.empty_like(real)
777
- for i, h in enumerate(harmonic):
778
- h = (h - 1) * 2 + 1
779
- re, im = phasor_from_polar(numpy.deg2rad(data[h]), data[h + 1])
780
- real[i] = re
781
- imag[i] = im
782
- if not keep_harmonic_dim:
783
- real = real.reshape(mean.shape)
784
- imag = imag.reshape(mean.shape)
785
-
786
- return mean, real, imag
787
-
788
-
789
- def read_lsm(
790
- filename: str | PathLike[Any],
791
- /,
792
- ) -> DataArray:
793
- """Return hyperspectral image and metadata from Zeiss LSM file.
794
-
795
- LSM files contain multi-dimensional images and metadata from laser
796
- scanning microscopy measurements. The file format is based on TIFF.
797
-
798
- Parameters
799
- ----------
800
- filename : str or Path
801
- Name of OME-TIFF file to read.
802
-
803
- Returns
804
- -------
805
- xarray.DataArray
806
- Hyperspectral image data.
807
- Usually, a 3-to-5-dimensional array of type ``uint8`` or ``uint16``.
808
-
809
- Raises
810
- ------
811
- tifffile.TiffFileError
812
- File is not a TIFF file.
813
- ValueError
814
- File is not an LSM file or does not contain hyperspectral image.
815
-
816
- Examples
817
- --------
818
- >>> data = read_lsm(fetch('paramecium.lsm'))
819
- >>> data.values
820
- array(...)
821
- >>> data.dtype
822
- dtype('uint8')
823
- >>> data.shape
824
- (30, 512, 512)
825
- >>> data.dims
826
- ('C', 'Y', 'X')
827
- >>> data.coords['C'].data # wavelengths
828
- array(...)
829
-
830
- """
831
- import tifffile
832
-
833
- with tifffile.TiffFile(filename) as tif:
834
- if not tif.is_lsm:
835
- raise ValueError(f'{tif.filename} is not an LSM file')
836
-
837
- page = tif.pages.first
838
- lsminfo = tif.lsm_metadata
839
- channels = page.tags[258].count
840
-
841
- if channels < 4 or lsminfo is None or lsminfo['SpectralScan'] != 1:
842
- raise ValueError(
843
- f'{tif.filename} does not contain hyperspectral image'
844
- )
845
-
846
- # TODO: contribute this to tifffile
847
- series = tif.series[0]
848
- data = series.asarray()
849
- dims = tuple(series.axes)
850
- coords = {}
851
- # channel wavelengths
852
- axis = dims.index('C')
853
- wavelengths = lsminfo['ChannelWavelength'].mean(axis=1)
854
- if wavelengths.size != data.shape[axis]:
855
- raise ValueError(
856
- f'{tif.filename} wavelengths do not match channel axis'
857
- )
858
- # stack may contain non-wavelength frame
859
- indices = wavelengths > 0
860
- wavelengths = wavelengths[indices]
861
- if wavelengths.size < 3:
862
- raise ValueError(
863
- f'{tif.filename} does not contain hyperspectral image'
864
- )
865
- data = data.take(indices.nonzero()[0], axis=axis)
866
- coords['C'] = wavelengths
867
- # time stamps
868
- if 'T' in dims:
869
- coords['T'] = lsminfo['TimeStamps']
870
- if coords['T'].size != data.shape[dims.index('T')]:
871
- raise ValueError(
872
- f'{tif.filename} timestamps do not match time axis'
873
- )
874
- # spatial coordinates
875
- for ax in 'ZYX':
876
- if ax in dims:
877
- size = data.shape[dims.index(ax)]
878
- coords[ax] = numpy.linspace(
879
- lsminfo[f'Origin{ax}'],
880
- size * lsminfo[f'VoxelSize{ax}'],
881
- size,
882
- endpoint=False,
883
- dtype=numpy.float64,
884
- )
885
- metadata = _metadata(series.axes, data.shape, filename, **coords)
886
-
887
- from xarray import DataArray
888
-
889
- return DataArray(data, **metadata)
890
-
891
-
892
- def read_imspector_tiff(
893
- filename: str | PathLike[Any],
894
- /,
895
- ) -> DataArray:
896
- """Return FLIM image stack and metadata from ImSpector TIFF file.
897
-
898
- Parameters
899
- ----------
900
- filename : str or Path
901
- Name of ImSpector FLIM TIFF file to read.
902
-
903
- Returns
904
- -------
905
- xarray.DataArray
906
- TCSPC image stack.
907
- Usually, a 3-to-5-dimensional array of type ``uint16``.
908
-
909
- - ``coords['H']``: times of histogram bins.
910
- - ``attrs['frequency']``: repetition frequency in MHz.
911
-
912
- Raises
913
- ------
914
- tifffile.TiffFileError
915
- File is not a TIFF file.
916
- ValueError
917
- File is not an ImSpector FLIM TIFF file.
918
-
919
- Examples
920
- --------
921
- >>> data = read_imspector_tiff(fetch('Embryo.tif'))
922
- >>> data.values
923
- array(...)
924
- >>> data.dtype
925
- dtype('uint16')
926
- >>> data.shape
927
- (56, 512, 512)
928
- >>> data.dims
929
- ('H', 'Y', 'X')
930
- >>> data.coords['H'].data # dtime bins
931
- array(...)
932
- >>> data.attrs['frequency'] # doctest: +NUMBER
933
- 80.109
934
-
935
- """
936
- from xml.etree import ElementTree
937
-
938
- import tifffile
939
-
940
- with tifffile.TiffFile(filename) as tif:
941
- tags = tif.pages.first.tags
942
- omexml = tags.valueof(270, '')
943
- make = tags.valueof(271, '')
944
-
945
- if (
946
- make != 'ImSpector'
947
- or not omexml.startswith('<?xml version')
948
- or len(tif.series) != 1
949
- or not tif.is_ome
950
- ):
951
- raise ValueError(f'{tif.filename} is not an ImSpector TIFF file')
952
-
953
- series = tif.series[0]
954
- ndim = series.ndim
955
- axes = series.axes
956
- shape = series.shape
957
-
958
- if ndim < 3 or not axes.endswith('YX'):
959
- raise ValueError(
960
- f'{tif.filename} is not an ImSpector FLIM TIFF file'
961
- )
962
-
963
- data = series.asarray()
964
-
965
- attrs: dict[str, Any] = {}
966
- coords = {}
967
- physical_size = {}
968
-
969
- root = ElementTree.fromstring(omexml)
970
- ns = {
971
- '': 'http://www.openmicroscopy.org/Schemas/OME/2008-02',
972
- 'ca': 'http://www.openmicroscopy.org/Schemas/CA/2008-02',
973
- }
974
-
975
- description = root.find('.//Description', ns)
976
- if (
977
- description is not None
978
- and description.text
979
- and description.text != 'not_specified'
980
- ):
981
- attrs['description'] = description.text
982
-
983
- pixels = root.find('.//Image/Pixels', ns)
984
- assert pixels is not None
985
- for ax in 'TZYX':
986
- attrib = 'TimeIncrement' if ax == 'T' else f'PhysicalSize{ax}'
987
- if ax not in axes or attrib not in pixels.attrib:
988
- continue
989
- size = float(pixels.attrib[attrib])
990
- physical_size[ax] = size
991
- coords[ax] = numpy.linspace(
992
- 0.0,
993
- size,
994
- shape[axes.index(ax)],
995
- endpoint=False,
996
- dtype=numpy.float64,
997
- )
998
-
999
- axes_labels = root.find('.//ca:CustomAttributes/AxesLabels', ns)
1000
- if (
1001
- axes_labels is None
1002
- or 'X' not in axes_labels.attrib
1003
- or 'TCSPC' not in axes_labels.attrib['X']
1004
- or 'FirstAxis' not in axes_labels.attrib
1005
- or 'SecondAxis' not in axes_labels.attrib
1006
- ):
1007
- raise ValueError(f'{tif.filename} is not an ImSpector FLIM TIFF file')
1008
-
1009
- if axes_labels.attrib['FirstAxis'].endswith('TCSPC T'):
1010
- ax = axes[-3]
1011
- assert axes_labels.attrib['FirstAxis-Unit'] == 'ns'
1012
- elif axes_labels.attrib['SecondAxis'].endswith('TCSPC T') and ndim > 3:
1013
- ax = axes[-4]
1014
- assert axes_labels.attrib['SecondAxis-Unit'] == 'ns'
1015
- else:
1016
- raise ValueError(f'{tif.filename} is not an ImSpector FLIM TIFF file')
1017
- axes = axes.replace(ax, 'H')
1018
- coords['H'] = coords[ax]
1019
- del coords[ax]
1020
-
1021
- attrs['frequency'] = float(
1022
- 1000.0 / (shape[axes.index('H')] * physical_size[ax])
1023
- )
1024
-
1025
- metadata = _metadata(axes, shape, filename, attrs=attrs, **coords)
1026
-
1027
- from xarray import DataArray
1028
-
1029
- return DataArray(data, **metadata)
1030
-
1031
-
1032
- def read_ifli(
1033
- filename: str | PathLike[Any],
1034
- /,
1035
- *,
1036
- channel: int = 0,
1037
- **kwargs: Any,
1038
- ) -> DataArray:
1039
- """Return image and metadata from ISS IFLI file.
1040
-
1041
- ISS VistaVision IFLI files contain phasor coordinates for several
1042
- positions, wavelengths, time points, channels, slices, and frequencies
1043
- from analog or digital frequency-domain fluorescence lifetime measurements.
1044
-
1045
- Parameters
1046
- ----------
1047
- filename : str or Path
1048
- Name of ISS IFLI file to read.
1049
- channel : int, optional
1050
- Index of channel to return. The first channel is returned by default.
1051
- **kwargs
1052
- Additional arguments passed to :py:meth:`lfdfiles.VistaIfli.asarray`,
1053
- for example ``memmap=True``.
1054
-
1055
- Returns
1056
- -------
1057
- xarray.DataArray
1058
- Average intensity and phasor coordinates.
1059
- An array of up to 8 dimensions with :ref:`axes codes <axes>`
1060
- ``'RCTZYXFS'`` and type ``float32``.
1061
- The last dimension contains `mean`, `real`, and `imag` phasor
1062
- coordinates.
1063
-
1064
- - ``coords['F']``: modulation frequencies.
1065
- - ``coords['C']``: emission wavelengths, if any.
1066
- - ``attrs['ref_tau']``: reference lifetimes.
1067
- - ``attrs['ref_tau_frac']``: reference lifetime fractions.
1068
- - ``attrs['ref_phasor']``: reference phasor coordinates for all
1069
- frequencies.
1070
-
1071
- Raises
1072
- ------
1073
- lfdfiles.LfdFileError
1074
- File is not an ISS IFLI file.
1075
-
1076
- Examples
1077
- --------
1078
- >>> data = read_ifli(fetch('frequency_domain.ifli'))
1079
- >>> data.values
1080
- array(...)
1081
- >>> data.dtype
1082
- dtype('float32')
1083
- >>> data.shape
1084
- (256, 256, 4, 3)
1085
- >>> data.dims
1086
- ('Y', 'X', 'F', 'S')
1087
- >>> data.coords['F'].data # doctest: +NUMBER
1088
- array([8.033e+07, 1.607e+08, 2.41e+08, 4.017e+08])
1089
- >>> data.coords['S'].data
1090
- array(['mean', 'real', 'imag'], dtype='<U4')
1091
- >>> data.attrs
1092
- {'ref_tau': (2.5, 0.0), 'ref_tau_frac': (1.0, 0.0), 'ref_phasor': array...}
1093
-
1094
- """
1095
- import lfdfiles
1096
-
1097
- with lfdfiles.VistaIfli(filename) as ifli:
1098
- assert ifli.axes is not None
1099
- # always return one acquisition channel to simplify metadata handling
1100
- data = ifli.asarray(**kwargs)[:, channel : channel + 1].copy()
1101
- shape, axes, _ = _squeeze_axes(data.shape, ifli.axes, skip='FYX')
1102
- axes = axes.replace('E', 'C') # spectral axis
1103
- data = data.reshape(shape)
1104
- header = ifli.header
1105
- coords: dict[str, Any] = {}
1106
- coords['S'] = ['mean', 'real', 'imag']
1107
- coords['F'] = numpy.array(header['ModFrequency'])
1108
- # TODO: how to distinguish time- from frequency-domain?
1109
- # TODO: how to extract spatial coordinates?
1110
- if 'T' in axes:
1111
- coords['T'] = numpy.array(header['TimeTags'])
1112
- if 'C' in axes:
1113
- coords['C'] = numpy.array(header['SpectrumInfo'])
1114
- # if 'Z' in axes:
1115
- # coords['Z'] = numpy.array(header[])
1116
- metadata = _metadata(axes, shape, filename, **coords)
1117
- attrs = metadata['attrs']
1118
- attrs['ref_tau'] = (
1119
- header['RefLifetime'][channel],
1120
- header['RefLifetime2'][channel],
1121
- )
1122
- attrs['ref_tau_frac'] = (
1123
- header['RefLifetimeFrac'][channel],
1124
- 1.0 - header['RefLifetimeFrac'][channel],
1125
- )
1126
- attrs['ref_phasor'] = numpy.array(header['RefDCPhasor'][channel])
1127
-
1128
- from xarray import DataArray
1129
-
1130
- return DataArray(data, **metadata)
1131
-
1132
-
1133
- def read_sdt(
1134
- filename: str | PathLike[Any],
1135
- /,
1136
- *,
1137
- index: int = 0,
1138
- ) -> DataArray:
1139
- """Return time-resolved image and metadata from Becker & Hickl SDT file.
1140
-
1141
- SDT files contain time-correlated single photon counting measurement data
1142
- and instrumentation parameters.
1143
-
1144
- Parameters
1145
- ----------
1146
- filename : str or Path
1147
- Name of SDT file to read.
1148
- index : int, optional, default: 0
1149
- Index of dataset to read in case the file contains multiple datasets.
1150
-
1151
- Returns
1152
- -------
1153
- xarray.DataArray
1154
- Time correlated single photon counting image data with
1155
- :ref:`axes codes <axes>` ``'YXH'`` and type ``uint16``, ``uint32``,
1156
- or ``float32``.
1157
-
1158
- - ``coords['H']``: times of the histogram bins.
1159
- - ``attrs['frequency']``: repetition frequency in MHz.
1160
-
1161
- Raises
1162
- ------
1163
- ValueError
1164
- File is not an SDT file containing time-correlated single photon
1165
- counting data.
1166
-
1167
- Examples
1168
- --------
1169
- >>> data = read_sdt(fetch('tcspc.sdt'))
1170
- >>> data.values
1171
- array(...)
1172
- >>> data.dtype
1173
- dtype('uint16')
1174
- >>> data.shape
1175
- (128, 128, 256)
1176
- >>> data.dims
1177
- ('Y', 'X', 'H')
1178
- >>> data.coords['H'].data
1179
- array(...)
1180
- >>> data.attrs['frequency'] # doctest: +NUMBER
1181
- 79.99
1182
-
1183
- """
1184
- import sdtfile
1185
-
1186
- with sdtfile.SdtFile(filename) as sdt:
1187
- if (
1188
- 'SPC Setup & Data File' not in sdt.info.id
1189
- and 'SPC FCS Data File' not in sdt.info.id
1190
- ):
1191
- # skip DLL data
1192
- raise ValueError(
1193
- f'{os.path.basename(filename)!r} '
1194
- 'is not an SDT file containing TCSPC data'
1195
- )
1196
- # filter block types?
1197
- # sdtfile.BlockType(sdt.block_headers[index].block_type).contents
1198
- # == 'PAGE_BLOCK'
1199
- data = sdt.data[index]
1200
- times = sdt.times[index]
1201
-
1202
- # TODO: get spatial coordinates from scanner settings?
1203
- metadata = _metadata('QYXH'[-data.ndim :], data.shape, filename, H=times)
1204
- metadata['attrs']['frequency'] = 1e-6 / float(times[-1] + times[1])
1205
-
1206
- from xarray import DataArray
1207
-
1208
- return DataArray(data, **metadata)
1209
-
1210
-
1211
- def read_ptu(
1212
- filename: str | PathLike[Any],
1213
- /,
1214
- selection: Sequence[int | slice | EllipsisType | None] | None = None,
1215
- *,
1216
- trimdims: Sequence[Literal['T', 'C', 'H']] | str | None = None,
1217
- dtype: DTypeLike | None = None,
1218
- frame: int | None = None,
1219
- channel: int | None = None,
1220
- dtime: int | None = 0,
1221
- keepdims: bool = True,
1222
- ) -> DataArray:
1223
- """Return image histogram and metadata from PicoQuant PTU T3 mode file.
1224
-
1225
- PTU files contain time-correlated single photon counting measurement data
1226
- and instrumentation parameters.
1227
-
1228
- Parameters
1229
- ----------
1230
- filename : str or Path
1231
- Name of PTU file to read.
1232
- selection : sequence of index types, optional
1233
- Indices for all dimensions:
1234
-
1235
- - ``None``: return all items along axis (default).
1236
- - ``Ellipsis``: return all items along multiple axes.
1237
- - ``int``: return single item along axis.
1238
- - ``slice``: return chunk of axis.
1239
- ``slice.step`` is binning factor.
1240
- If ``slice.step=-1``, integrate all items along axis.
1241
-
1242
- trimdims : str, optional, default: 'TCH'
1243
- Axes to trim.
1244
- dtype : dtype-like, optional, default: uint16
1245
- Unsigned integer type of image histogram array.
1246
- Increase the bit depth to avoid overflows when integrating.
1247
- frame : int, optional
1248
- If < 0, integrate time axis, else return specified frame.
1249
- Overrides `selection` for axis ``T``.
1250
- channel : int, optional
1251
- If < 0, integrate channel axis, else return specified channel.
1252
- Overrides `selection` for axis ``C``.
1253
- dtime : int, optional, default: 0
1254
- Specifies number of bins in image histogram.
1255
- If 0 (default), return number of bins in one period.
1256
- If < 0, integrate delay time axis.
1257
- If > 0, return up to specified bin.
1258
- Overrides `selection` for axis ``H``.
1259
- keepdims : bool, optional, default: True
1260
- If true (default), reduced axes are left as size-one dimension.
1261
-
1262
- Returns
1263
- -------
1264
- xarray.DataArray
1265
- Decoded TTTR T3 records as up to 5-dimensional image array
1266
- with :ref:`axes codes <axes>` ``'TYXCH'`` and type specified
1267
- in ``dtype``:
1268
-
1269
- - ``coords['H']``: times of the histogram bins.
1270
- - ``attrs['frequency']``: repetition frequency in MHz.
1271
-
1272
- Raises
1273
- ------
1274
- ptufile.PqFileError
1275
- File is not a PicoQuant PTU file or is corrupted.
1276
- ValueError
1277
- File is not a PicoQuant PTU T3 mode file containing time-correlated
1278
- single photon counting data.
1279
-
1280
- Examples
1281
- --------
1282
- >>> data = read_ptu(fetch('hazelnut_FLIM_single_image.ptu'))
1283
- >>> data.values
1284
- array(...)
1285
- >>> data.dtype
1286
- dtype('uint16')
1287
- >>> data.shape
1288
- (5, 256, 256, 1, 132)
1289
- >>> data.dims
1290
- ('T', 'Y', 'X', 'C', 'H')
1291
- >>> data.coords['H'].data
1292
- array(...)
1293
- >>> data.attrs['frequency'] # doctest: +NUMBER
1294
- 78.02
1295
-
1296
- """
1297
- import ptufile
1298
- from xarray import DataArray
1299
-
1300
- with ptufile.PtuFile(filename, trimdims=trimdims) as ptu:
1301
- if not ptu.is_t3 or not ptu.is_image:
1302
- raise ValueError(
1303
- f'{os.path.basename(filename)!r} '
1304
- 'is not a PTU file containing a T3 mode image'
1305
- )
1306
- data = ptu.decode_image(
1307
- selection,
1308
- dtype=dtype,
1309
- frame=frame,
1310
- channel=channel,
1311
- dtime=dtime,
1312
- keepdims=keepdims,
1313
- asxarray=True,
1314
- )
1315
- assert isinstance(data, DataArray)
1316
- data.attrs['frequency'] = ptu.frequency * 1e-6 # MHz
1317
-
1318
- return data
1319
-
1320
-
1321
- def read_flif(
1322
- filename: str | PathLike[Any],
1323
- /,
1324
- ) -> DataArray:
1325
- """Return frequency-domain image and metadata from FlimFast FLIF file.
1326
-
1327
- FlimFast FLIF files contain camera images and metadata from
1328
- frequency-domain fluorescence lifetime measurements.
1329
-
1330
- Parameters
1331
- ----------
1332
- filename : str or Path
1333
- Name of FlimFast FLIF file to read.
1334
-
1335
- Returns
1336
- -------
1337
- xarray.DataArray
1338
- Frequency-domain phase images with :ref:`axes codes <axes>` ``'THYX'``
1339
- and type ``uint16``:
1340
-
1341
- - ``coords['H']``: phases in radians.
1342
- - ``attrs['frequency']``: repetition frequency in MHz.
1343
- - ``attrs['ref_phase']``: measured phase of reference.
1344
- - ``attrs['ref_mod']``: measured modulation of reference.
1345
- - ``attrs['ref_tauphase']``: lifetime from phase of reference.
1346
- - ``attrs['ref_taumod']``: lifetime from modulation of reference.
1347
-
1348
- Raises
1349
- ------
1350
- lfdfiles.LfdFileError
1351
- File is not a FlimFast FLIF file.
1352
-
1353
- Examples
1354
- --------
1355
- >>> data = read_flif(fetch('flimfast.flif'))
1356
- >>> data.values
1357
- array(...)
1358
- >>> data.dtype
1359
- dtype('uint16')
1360
- >>> data.shape
1361
- (32, 220, 300)
1362
- >>> data.dims
1363
- ('H', 'Y', 'X')
1364
- >>> data.coords['H'].data
1365
- array(...)
1366
- >>> data.attrs['frequency'] # doctest: +NUMBER
1367
- 80.65
1368
-
1369
- """
1370
- import lfdfiles
1371
-
1372
- with lfdfiles.FlimfastFlif(filename) as flif:
1373
- nphases = int(flif.header.phases)
1374
- data = flif.asarray()
1375
- if data.shape[0] < nphases:
1376
- raise ValueError(f'measured phases {data.shape[0]} < {nphases=}')
1377
- if data.shape[0] % nphases != 0:
1378
- data = data[: (data.shape[0] // nphases) * nphases]
1379
- data = data.reshape(-1, nphases, data.shape[1], data.shape[2])
1380
- if data.shape[0] == 1:
1381
- data = data[0]
1382
- axes = 'HYX'
1383
- else:
1384
- axes = 'THYX'
1385
- # TODO: check if phases are ordered
1386
- phases = numpy.radians(flif.records['phase'][:nphases])
1387
- metadata = _metadata(axes, data.shape, H=phases)
1388
- attrs = metadata['attrs']
1389
- attrs['frequency'] = float(flif.header.frequency)
1390
- attrs['ref_phase'] = float(flif.header.measured_phase)
1391
- attrs['ref_mod'] = float(flif.header.measured_mod)
1392
- attrs['ref_tauphase'] = float(flif.header.ref_tauphase)
1393
- attrs['ref_taumod'] = float(flif.header.ref_taumod)
1394
-
1395
- from xarray import DataArray
1396
-
1397
- return DataArray(data, **metadata)
1398
-
1399
-
1400
- def read_fbd(
1401
- filename: str | PathLike[Any],
1402
- /,
1403
- *,
1404
- frame: int | None = None,
1405
- channel: int | None = None,
1406
- keepdims: bool = True,
1407
- laser_factor: float = -1.0,
1408
- ) -> DataArray:
1409
- """Return frequency-domain image and metadata from FLIMbox FBD file.
1410
-
1411
- FDB files contain encoded data from the FLIMbox device, which can be
1412
- decoded to photon arrival windows, channels, and global times.
1413
- The encoding scheme depends on the FLIMbox device's firmware.
1414
- The FBD file format is undocumented.
1415
-
1416
- This function may fail to produce expected results when files use unknown
1417
- firmware, do not contain image scans, settings were recorded incorrectly,
1418
- scanner and FLIMbox frequencies were out of sync, or scanner settings were
1419
- changed during acquisition.
1420
-
1421
- Parameters
1422
- ----------
1423
- filename : str or Path
1424
- Name of FLIMbox FBD file to read.
1425
- frame : int, optional
1426
- If None (default), return all frames.
1427
- If < 0, integrate time axis, else return specified frame.
1428
- channel : int, optional
1429
- If None (default), return all channels, else return specified channel.
1430
- keepdims : bool, optional
1431
- If true (default), reduced axes are left as size-one dimension.
1432
- laser_factor : float, optional
1433
- Factor to correct dwell_time/laser_frequency.
1434
-
1435
- Returns
1436
- -------
1437
- xarray.DataArray
1438
- Frequency-domain image histogram with :ref:`axes codes <axes>`
1439
- ``'TCYXH'`` and type ``uint16``:
1440
-
1441
- - ``coords['H']``: phases in radians.
1442
- - ``attrs['frequency']``: repetition frequency in MHz.
1443
-
1444
- Raises
1445
- ------
1446
- lfdfiles.LfdFileError
1447
- File is not a FLIMbox FBD file.
1448
-
1449
- Examples
1450
- --------
1451
- >>> data = read_fbd(fetch('convallaria_000$EI0S.fbd')) # doctest: +SKIP
1452
- >>> data.values # doctest: +SKIP
1453
- array(...)
1454
- >>> data.dtype # doctest: +SKIP
1455
- dtype('uint16')
1456
- >>> data.shape # doctest: +SKIP
1457
- (9, 2, 256, 256, 64)
1458
- >>> data.dims # doctest: +SKIP
1459
- ('T', 'C', 'Y', 'X', 'H')
1460
- >>> data.coords['H'].data # doctest: +SKIP
1461
- array(...)
1462
- >>> data.attrs['frequency'] # doctest: +SKIP
1463
- 40.0
1464
-
1465
- """
1466
- import lfdfiles
1467
-
1468
- integrate_frames = 0 if frame is None or frame >= 0 else 1
1469
-
1470
- with lfdfiles.FlimboxFbd(filename, laser_factor=laser_factor) as fbd:
1471
- data = fbd.asimage(None, None, integrate_frames=integrate_frames)
1472
- if integrate_frames:
1473
- frame = None
1474
- copy = False
1475
- axes = 'TCYXH'
1476
- if channel is None:
1477
- if not keepdims and data.shape[1] == 1:
1478
- data = data[:, 0]
1479
- axes = 'TYXH'
1480
- else:
1481
- if channel < 0 or channel >= data.shape[1]:
1482
- raise IndexError(f'{channel=} out of bounds')
1483
- if keepdims:
1484
- data = data[:, channel : channel + 1]
1485
- else:
1486
- data = data[:, channel]
1487
- axes = 'TYXH'
1488
- copy = True
1489
- if frame is None:
1490
- if not keepdims and data.shape[0] == 1:
1491
- data = data[0]
1492
- axes = axes[1:]
1493
- else:
1494
- if frame < 0 or frame > data.shape[0]:
1495
- raise IndexError(f'{frame=} out of bounds')
1496
- if keepdims:
1497
- data = data[frame : frame + 1]
1498
- else:
1499
- data = data[frame]
1500
- axes = axes[1:]
1501
- copy = True
1502
- if copy:
1503
- data = data.copy()
1504
- # TODO: return arrival window indices or micro-times as H coords?
1505
- phases = numpy.linspace(
1506
- 0.0, numpy.pi * 2, data.shape[-1], endpoint=False
1507
- )
1508
- metadata = _metadata(axes, data.shape, H=phases)
1509
- attrs = metadata['attrs']
1510
- attrs['frequency'] = fbd.laser_frequency * 1e-6
1511
-
1512
- from xarray import DataArray
1513
-
1514
- return DataArray(data, **metadata)
1515
-
1516
-
1517
- def read_b64(
1518
- filename: str | PathLike[Any],
1519
- /,
1520
- ) -> DataArray:
1521
- """Return intensity image and metadata from SimFCS B64 file.
1522
-
1523
- B64 files contain one or more square intensity image(s), a carpet
1524
- of lines, or a stream of intensity data. B64 files contain no metadata.
1525
-
1526
- Parameters
1527
- ----------
1528
- filename : str or Path
1529
- Name of SimFCS B64 file to read.
1530
-
1531
- Returns
1532
- -------
1533
- xarray.DataArray
1534
- Stack of square-sized intensity images of type ``int16``.
1535
-
1536
- Raises
1537
- ------
1538
- lfdfiles.LfdFileError
1539
- File is not a SimFCS B64 file.
1540
- ValueError
1541
- File does not contain an image stack.
1542
-
1543
- Examples
1544
- --------
1545
- >>> data = read_b64(fetch('simfcs.b64'))
1546
- >>> data.values
1547
- array(...)
1548
- >>> data.dtype
1549
- dtype('int16')
1550
- >>> data.shape
1551
- (22, 1024, 1024)
1552
- >>> data.dtype
1553
- dtype('int16')
1554
- >>> data.dims
1555
- ('I', 'Y', 'X')
1556
-
1557
- """
1558
- import lfdfiles
1559
-
1560
- with lfdfiles.SimfcsB64(filename) as b64:
1561
- data = b64.asarray()
1562
- if data.ndim != 3:
1563
- raise ValueError(
1564
- f'{os.path.basename(filename)!r} '
1565
- 'does not contain an image stack'
1566
- )
1567
- metadata = _metadata(b64.axes, data.shape, filename)
1568
-
1569
- from xarray import DataArray
1570
-
1571
- return DataArray(data, **metadata)
1572
-
1573
-
1574
- def read_z64(
1575
- filename: str | PathLike[Any],
1576
- /,
1577
- ) -> DataArray:
1578
- """Return image and metadata from SimFCS Z64 file.
1579
-
1580
- Z64 files contain stacks of square images such as intensity volumes
1581
- or time-domain fluorescence lifetime histograms acquired from
1582
- Becker & Hickl(r) TCSPC cards. Z64 files contain no metadata.
1583
-
1584
- Parameters
1585
- ----------
1586
- filename : str or Path
1587
- Name of SimFCS Z64 file to read.
1588
-
1589
- Returns
1590
- -------
1591
- xarray.DataArray
1592
- Single or stack of square-sized images of type ``float32``.
1593
-
1594
- Raises
1595
- ------
1596
- lfdfiles.LfdFileError
1597
- File is not a SimFCS Z64 file.
1598
-
1599
- Examples
1600
- --------
1601
- >>> data = read_z64(fetch('simfcs.z64'))
1602
- >>> data.values
1603
- array(...)
1604
- >>> data.dtype
1605
- dtype('float32')
1606
- >>> data.shape
1607
- (256, 256, 256)
1608
- >>> data.dims
1609
- ('Q', 'Y', 'X')
1610
-
1611
- """
1612
- import lfdfiles
1613
-
1614
- with lfdfiles.SimfcsZ64(filename) as z64:
1615
- data = z64.asarray()
1616
- metadata = _metadata(z64.axes, data.shape, filename)
1617
-
1618
- from xarray import DataArray
1619
-
1620
- return DataArray(data, **metadata)
1621
-
1622
-
1623
- def read_bh(
1624
- filename: str | PathLike[Any],
1625
- /,
1626
- ) -> DataArray:
1627
- """Return image and metadata from SimFCS B&H file.
1628
-
1629
- B&H files contain time-domain fluorescence lifetime histogram data,
1630
- acquired from Becker & Hickl(r) TCSPC cards, or converted from other
1631
- data sources. B&H files contain no metadata.
1632
-
1633
- Parameters
1634
- ----------
1635
- filename : str or Path
1636
- Name of SimFCS B&H file to read.
1637
-
1638
- Returns
1639
- -------
1640
- xarray.DataArray
1641
- Time-domain fluorescence lifetime histogram with axes ``'HYX'``,
1642
- shape ``(256, 256, 256)``, and type ``float32``.
1643
-
1644
- Raises
1645
- ------
1646
- lfdfiles.LfdFileError
1647
- File is not a SimFCS B&H file.
1648
-
1649
- Examples
1650
- --------
1651
- >>> data = read_bh(fetch('simfcs.b&h'))
1652
- >>> data.values
1653
- array(...)
1654
- >>> data.dtype
1655
- dtype('float32')
1656
- >>> data.shape
1657
- (256, 256, 256)
1658
- >>> data.dims
1659
- ('H', 'Y', 'X')
1660
-
1661
- """
1662
- import lfdfiles
1663
-
1664
- with lfdfiles.SimfcsBh(filename) as bnh:
1665
- assert bnh.axes is not None
1666
- data = bnh.asarray()
1667
- metadata = _metadata(bnh.axes.replace('Q', 'H'), data.shape, filename)
1668
-
1669
- from xarray import DataArray
1670
-
1671
- return DataArray(data, **metadata)
1672
-
1673
-
1674
- def read_bhz(
1675
- filename: str | PathLike[Any],
1676
- /,
1677
- ) -> DataArray:
1678
- """Return image and metadata from SimFCS BHZ file.
1679
-
1680
- BHZ files contain time-domain fluorescence lifetime histogram data,
1681
- acquired from Becker & Hickl(r) TCSPC cards, or converted from other
1682
- data sources. BHZ files contain no metadata.
1683
-
1684
- Parameters
1685
- ----------
1686
- filename : str or Path
1687
- Name of SimFCS BHZ file to read.
1688
-
1689
- Returns
1690
- -------
1691
- xarray.DataArray
1692
- Time-domain fluorescence lifetime histogram with axes ``'HYX'``,
1693
- shape ``(256, 256, 256)``, and type ``float32``.
1694
-
1695
- Raises
1696
- ------
1697
- lfdfiles.LfdFileError
1698
- File is not a SimFCS BHZ file.
1699
-
1700
- Examples
1701
- --------
1702
- >>> data = read_bhz(fetch('simfcs.bhz'))
1703
- >>> data.values
1704
- array(...)
1705
- >>> data.dtype
1706
- dtype('float32')
1707
- >>> data.shape
1708
- (256, 256, 256)
1709
- >>> data.dims
1710
- ('H', 'Y', 'X')
1711
-
1712
- """
1713
- import lfdfiles
1714
-
1715
- with lfdfiles.SimfcsBhz(filename) as bhz:
1716
- assert bhz.axes is not None
1717
- data = bhz.asarray()
1718
- metadata = _metadata(bhz.axes.replace('Q', 'H'), data.shape, filename)
1719
-
1720
- from xarray import DataArray
1721
-
1722
- return DataArray(data, **metadata)
1723
-
1724
-
1725
- def _metadata(
1726
- dims: Sequence[str] | None,
1727
- shape: tuple[int, ...],
1728
- /,
1729
- name: str | PathLike[Any] | None = None,
1730
- attrs: dict[str, Any] | None = None,
1731
- **coords: Any,
1732
- ) -> dict[str, Any]:
1733
- """Return xarray-style dims, coords, and attrs in a dict.
1734
-
1735
- >>> _metadata('SYX', (3, 2, 1), S=['0', '1', '2'])
1736
- {'dims': ('S', 'Y', 'X'), 'coords': {'S': ['0', '1', '2']}, 'attrs': {}}
1737
-
1738
- """
1739
- assert dims is not None
1740
- dims = tuple(dims)
1741
- if len(dims) != len(shape):
1742
- raise ValueError(
1743
- f'dims do not match shape {len(dims)} != {len(shape)}'
1744
- )
1745
- coords = {dim: coords[dim] for dim in dims if dim in coords}
1746
- if attrs is None:
1747
- attrs = {}
1748
- metadata = {'dims': dims, 'coords': coords, 'attrs': attrs}
1749
- if name:
1750
- metadata['name'] = os.path.basename(name)
1751
- return metadata
1752
-
1753
-
1754
- def _squeeze_axes(
1755
- shape: Sequence[int],
1756
- axes: str,
1757
- /,
1758
- skip: str = 'XY',
1759
- ) -> tuple[tuple[int, ...], str, tuple[bool, ...]]:
1760
- """Return shape and axes with length-1 dimensions removed.
1761
-
1762
- Remove unused dimensions unless their axes are listed in `skip`.
1763
-
1764
- Adapted from the tifffile library.
1765
-
1766
- Parameters
1767
- ----------
1768
- shape : tuple of ints
1769
- Sequence of dimension sizes.
1770
- axes : str
1771
- Character codes for dimensions in `shape`.
1772
- skip : str, optional
1773
- Character codes for dimensions whose length-1 dimensions are
1774
- not removed. The default is 'XY'.
1775
-
1776
- Returns
1777
- -------
1778
- shape : tuple of ints
1779
- Sequence of dimension sizes with length-1 dimensions removed.
1780
- axes : str
1781
- Character codes for dimensions in output `shape`.
1782
- squeezed : str
1783
- Dimensions were kept (True) or removed (False).
1784
-
1785
- Examples
1786
- --------
1787
- >>> _squeeze_axes((5, 1, 2, 1, 1), 'TZYXC')
1788
- ((5, 2, 1), 'TYX', (True, False, True, True, False))
1789
- >>> _squeeze_axes((1,), 'Q')
1790
- ((1,), 'Q', (True,))
1791
-
1792
- """
1793
- if len(shape) != len(axes):
1794
- raise ValueError(f'{len(shape)=} != {len(axes)=}')
1795
- if not axes:
1796
- return tuple(shape), axes, ()
1797
- squeezed: list[bool] = []
1798
- shape_squeezed: list[int] = []
1799
- axes_squeezed: list[str] = []
1800
- for size, ax in zip(shape, axes):
1801
- if size > 1 or ax in skip:
1802
- squeezed.append(True)
1803
- shape_squeezed.append(size)
1804
- axes_squeezed.append(ax)
1805
- else:
1806
- squeezed.append(False)
1807
- if len(shape_squeezed) == 0:
1808
- squeezed[-1] = True
1809
- shape_squeezed.append(shape[-1])
1810
- axes_squeezed.append(axes[-1])
1811
- return tuple(shape_squeezed), ''.join(axes_squeezed), tuple(squeezed)
4
+ from ._io import *
5
+ from ._io import __all__, __doc__