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