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