phasorpy 0.5__cp312-cp312-win_amd64.whl → 0.7__cp312-cp312-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,138 @@
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_pqbin` - PicoQuant BIN
12
+ - :py:func:`signal_from_sdt` - Becker & Hickl SDT
13
+ - :py:func:`signal_from_fbd` - FLIMbox FBD
14
+ - :py:func:`signal_from_flimlabs_json` - FLIM LABS JSON
15
+ - :py:func:`signal_from_imspector_tiff` - ImSpector FLIM TIFF
16
+ - :py:func:`signal_from_flif` - FlimFast FLIF
17
+ - :py:func:`signal_from_b64` - SimFCS B64
18
+ - :py:func:`signal_from_z64` - SimFCS Z64
19
+ - :py:func:`signal_from_bhz` - SimFCS BHZ
20
+ - :py:func:`signal_from_bh` - SimFCS B&H
21
+
22
+ - read phasor coordinates, lifetime images, and metadata from
23
+ specialized file formats:
24
+
25
+ - :py:func:`phasor_from_ometiff` - PhasorPy OME-TIFF
26
+ - :py:func:`phasor_from_ifli` - ISS IFLI
27
+ - :py:func:`phasor_from_lif` - Leica LIF and XLEF
28
+ - :py:func:`phasor_from_flimlabs_json` - FLIM LABS JSON
29
+ - :py:func:`phasor_from_simfcs_referenced` - SimFCS REF and R64
30
+ - :py:func:`lifetime_from_lif` - Leica LIF and XLEF
31
+
32
+ - write phasor coordinate images to OME-TIFF and SimFCS file formats:
33
+
34
+ - :py:func:`phasor_to_ometiff`
35
+ - :py:func:`phasor_to_simfcs_referenced`
36
+
37
+ Support for other file formats is being considered:
38
+
39
+ - OME-TIFF
40
+ - Zeiss CZI
41
+ - Nikon ND2
42
+ - Olympus OIB/OIF
43
+ - Olympus OIR
44
+
45
+ The functions are implemented as minimal wrappers around specialized
46
+ third-party file reader libraries, currently
47
+ `tifffile <https://github.com/cgohlke/tifffile>`_,
48
+ `ptufile <https://github.com/cgohlke/ptufile>`_,
49
+ `liffile <https://github.com/cgohlke/liffile>`_,
50
+ `sdtfile <https://github.com/cgohlke/sdtfile>`_, and
51
+ `lfdfiles <https://github.com/cgohlke/lfdfiles>`_.
52
+ For advanced or unsupported use cases, consider using these libraries directly.
53
+
54
+ The signal-reading functions typically have the following signature::
55
+
56
+ signal_from_ext(
57
+ filename: str | PathLike,
58
+ /,
59
+ **kwargs
60
+ ): -> xarray.DataArray
61
+
62
+ where ``ext`` indicates the file format and ``kwargs`` are optional arguments
63
+ passed to the underlying file reader library or used to select which data is
64
+ returned. The returned `xarray.DataArray
65
+ <https://docs.xarray.dev/en/stable/user-guide/data-structures.html>`_
66
+ contains an N-dimensional array with labeled coordinates, dimensions, and
67
+ attributes:
68
+
69
+ - ``data`` or ``values`` (*array_like*)
70
+
71
+ Numpy array or array-like holding the array's values.
72
+
73
+ - ``dims`` (*tuple of str*)
74
+
75
+ :ref:`Axes character codes <axes>` for each dimension in ``data``.
76
+ For example, ``('T', 'C', 'Y', 'X')`` defines the dimension order in a
77
+ 4-dimensional array of a time-series of multi-channel images.
78
+
79
+ - ``coords`` (*dict_like[str, array_like]*)
80
+
81
+ Coordinate arrays labelling each point in the data array.
82
+ The keys are :ref:`axes character codes <axes>`.
83
+ Values are 1-dimensional arrays of numbers or strings.
84
+ For example, ``coords['C']`` could be an array of emission wavelengths.
85
+
86
+ - ``attrs`` (*dict[str, Any]*)
87
+
88
+ Arbitrary metadata such as measurement or calibration parameters required to
89
+ interpret the data values.
90
+ For example, the laser repetition frequency of a time-resolved measurement.
91
+
92
+ .. _axes:
93
+
94
+ Axes character codes from the OME model and tifffile library are used as
95
+ ``dims`` items and ``coords`` keys:
96
+
97
+ - ``'X'`` : width (OME)
98
+ - ``'Y'`` : height (OME)
99
+ - ``'Z'`` : depth (OME)
100
+ - ``'S'`` : sample (color components or phasor coordinates)
101
+ - ``'I'`` : sequence (of images, frames, or planes)
102
+ - ``'T'`` : time (OME)
103
+ - ``'C'`` : channel (OME. Acquisition path or emission wavelength)
104
+ - ``'A'`` : angle (OME)
105
+ - ``'P'`` : phase (OME. In LSM, ``'P'`` maps to position)
106
+ - ``'R'`` : tile (OME. Region, position, or mosaic)
107
+ - ``'H'`` : lifetime histogram (OME)
108
+ - ``'E'`` : lambda (OME. Excitation wavelength)
109
+ - ``'F'`` : frequency (ISS)
110
+ - ``'Q'`` : other (OME. Harmonics in PhasorPy TIFF)
111
+ - ``'L'`` : exposure (FluoView)
112
+ - ``'V'`` : event (FluoView)
113
+ - ``'M'`` : mosaic (LSM 6)
114
+ - ``'J'`` : column (NDTiff)
115
+ - ``'K'`` : row (NDTiff)
116
+
117
+ """
118
+
119
+ from __future__ import annotations
120
+
121
+ __all__: list[str] = []
122
+
123
+ from .._utils import init_module
124
+ from ._flimlabs import *
125
+ from ._leica import *
126
+ from ._ometiff import *
127
+ from ._other import *
128
+ from ._simfcs import *
129
+
130
+ # The `init_module()` function dynamically populates the `__all__` list with
131
+ # all public symbols imported from submodules or defined in this module.
132
+ # Any name not starting with an underscore will be automatically exported
133
+ # when using "from phasorpy.io import *"
134
+
135
+ init_module(globals())
136
+ del init_module
137
+
138
+ # flake8: noqa: F401, F403
@@ -0,0 +1,360 @@
1
+ """Read FLIM LABS file formats."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = ['phasor_from_flimlabs_json', 'signal_from_flimlabs_json']
6
+
7
+ import json
8
+ from typing import TYPE_CHECKING
9
+
10
+ from .._utils import parse_harmonic, xarray_metadata
11
+
12
+ if TYPE_CHECKING:
13
+ from .._typing import (
14
+ Any,
15
+ DataArray,
16
+ DTypeLike,
17
+ Literal,
18
+ NDArray,
19
+ PathLike,
20
+ Sequence,
21
+ )
22
+
23
+ import numpy
24
+
25
+
26
+ def phasor_from_flimlabs_json(
27
+ filename: str | PathLike[Any],
28
+ /,
29
+ channel: int | None = 0,
30
+ harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
31
+ ) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], dict[str, Any]]:
32
+ """Return phasor coordinates and metadata from FLIM LABS JSON phasor file.
33
+
34
+ FLIM LABS JSON files may contain calibrated phasor coordinates
35
+ (possibly for multiple channels and harmonics) and metadata from
36
+ digital frequency-domain measurements.
37
+
38
+ Parameters
39
+ ----------
40
+ filename : str or Path
41
+ Name of FLIM LABS JSON phasor file to read.
42
+ The file name usually contains the string "_phasor".
43
+ channel : int, optional
44
+ Index of channel to return.
45
+ By default, return the first channel.
46
+ If None, return all channels.
47
+ harmonic : int, sequence of int, or 'all', optional
48
+ Harmonic(s) to return from file.
49
+ If None (default), return the first harmonic stored in the file.
50
+ If `'all'`, return all harmonics as stored in file.
51
+ If a list, the first axes of the returned `real` and `imag` arrays
52
+ contain specified harmonic(s).
53
+ If an integer, the returned `real` and `imag` arrays are single
54
+ harmonic and have the same shape as `mean`.
55
+
56
+ Returns
57
+ -------
58
+ mean : ndarray
59
+ Average intensity image.
60
+ Zeroed if an intensity image is not present in file.
61
+ real : ndarray
62
+ Image of real component of phasor coordinates.
63
+ imag : ndarray
64
+ Image of imaginary component of phasor coordinates.
65
+ attrs : dict
66
+ Select metadata:
67
+
68
+ - ``'dims'`` (tuple of str):
69
+ :ref:`Axes codes <axes>` for `mean` image dimensions.
70
+ - ``'samples'`` (int):
71
+ Number of time bins (always 256).
72
+ - ``'harmonic'`` (int or list of int):
73
+ Harmonic(s) of `real` and `imag`.
74
+ Single int if one harmonic, list if multiple harmonics.
75
+ - ``'frequency'`` (float):
76
+ Laser repetition frequency in MHz.
77
+ - ``'flimlabs_header'`` (dict):
78
+ FLIM LABS file header.
79
+
80
+ Raises
81
+ ------
82
+ ValueError
83
+ File is not a FLIM LABS JSON file containing phasor coordinates.
84
+ IndexError
85
+ Harmonic or channel not found in file.
86
+
87
+ See Also
88
+ --------
89
+ phasorpy.io.signal_from_flimlabs_json
90
+
91
+ Examples
92
+ --------
93
+ >>> mean, real, imag, attrs = phasor_from_flimlabs_json(
94
+ ... fetch('Convallaria_m2_1740751781_phasor_ch1.json'), harmonic='all'
95
+ ... )
96
+ >>> real.shape
97
+ (3, 256, 256)
98
+ >>> attrs['dims']
99
+ ('Y', 'X')
100
+ >>> attrs['harmonic']
101
+ [1, 2, 3]
102
+ >>> attrs['frequency'] # doctest: +NUMBER
103
+ 40.00
104
+
105
+ """
106
+ with open(filename, 'rb') as fh:
107
+ try:
108
+ data = json.load(fh)
109
+ except Exception as exc:
110
+ raise ValueError('not a valid JSON file') from exc
111
+
112
+ if (
113
+ 'header' not in data
114
+ or 'phasors_data' not in data
115
+ or 'laser_period_ns' not in data['header']
116
+ or 'file_id' not in data['header']
117
+ # or data['header']['file_id'] != [73, 80, 71, 49] # 'IPG1'
118
+ ):
119
+ raise ValueError(
120
+ 'not a FLIM LABS JSON file containing phasor coordinates'
121
+ )
122
+
123
+ header = data['header']
124
+ phasor_data = data['phasors_data']
125
+
126
+ harmonics = []
127
+ channels = [] # 1-based
128
+ for d in phasor_data:
129
+ h = d['harmonic']
130
+ if h not in harmonics:
131
+ harmonics.append(h)
132
+ c = d['channel']
133
+ if c not in channels:
134
+ channels.append(c)
135
+ harmonics = sorted(harmonics)
136
+ channels = sorted(channels)
137
+
138
+ if channel is not None:
139
+ if channel + 1 not in channels:
140
+ raise IndexError(f'{channel=}')
141
+ channel += 1 # 1-based index
142
+
143
+ if isinstance(harmonic, str) and harmonic == 'all':
144
+ harmonic = harmonics
145
+ keep_harmonic_axis = True
146
+ else:
147
+ harmonic, keep_harmonic_axis = parse_harmonic(harmonic, harmonics[-1])
148
+ if any(h not in harmonics for h in harmonic):
149
+ raise IndexError(f'{harmonic=} not in {harmonics!r}')
150
+ harmonic_index = {h: i for i, h in enumerate(harmonic)}
151
+
152
+ nharmonics = len(harmonic)
153
+ nchannels = len(channels) if channel is None else 1
154
+ height = header['image_height']
155
+ width = header['image_width']
156
+ dtype = numpy.float32
157
+
158
+ shape: tuple[int, ...] = nharmonics, nchannels, height, width
159
+ axes: str = 'CYX'
160
+ mean = numpy.zeros(shape[1:], dtype)
161
+ real = numpy.zeros(shape, dtype)
162
+ imag = numpy.zeros(shape, dtype)
163
+
164
+ for d in phasor_data:
165
+ h = d['harmonic']
166
+ if h not in harmonic_index:
167
+ continue
168
+ h = harmonic_index[h]
169
+ if channel is not None:
170
+ if d['channel'] != channel:
171
+ continue
172
+ c = 0
173
+ else:
174
+ c = channels.index(d['channel'])
175
+
176
+ real[h, c] = numpy.asarray(d['g_data'], dtype)
177
+ imag[h, c] = numpy.asarray(d['s_data'], dtype)
178
+
179
+ if 'intensities_data' in data:
180
+ from .._phasorpy import _flimlabs_mean
181
+
182
+ mean.shape = nchannels, height * width
183
+ _flimlabs_mean(
184
+ mean,
185
+ data['intensities_data'],
186
+ -1 if channel is None else channels.index(channel),
187
+ )
188
+ mean.shape = shape[1:]
189
+ # JSON cannot store NaN values
190
+ nan_mask = mean == 0
191
+ real[:, nan_mask] = numpy.nan
192
+ imag[:, nan_mask] = numpy.nan
193
+ del nan_mask
194
+
195
+ if nchannels == 1:
196
+ axes = axes[1:]
197
+ mean = mean[0]
198
+ real = real[:, 0]
199
+ imag = imag[:, 0]
200
+
201
+ if not keep_harmonic_axis:
202
+ real = real[0]
203
+ imag = imag[0]
204
+
205
+ attrs = {
206
+ 'dims': tuple(axes),
207
+ 'samples': 256,
208
+ 'harmonic': harmonic if keep_harmonic_axis else harmonic[0],
209
+ 'frequency': 1000.0 / header['laser_period_ns'],
210
+ 'flimlabs_header': header,
211
+ }
212
+
213
+ return mean, real, imag, attrs
214
+
215
+
216
+ def signal_from_flimlabs_json(
217
+ filename: str | PathLike[Any],
218
+ /,
219
+ *,
220
+ channel: int | None = 0,
221
+ dtype: DTypeLike | None = None,
222
+ ) -> DataArray:
223
+ """Return TCSPC histogram and metadata from FLIM LABS JSON imaging file.
224
+
225
+ FLIM LABS JSON imaging files contain encoded, multi-channel TCSPC
226
+ histograms and metadata from digital frequency-domain measurements.
227
+
228
+ Parameters
229
+ ----------
230
+ filename : str or Path
231
+ Name of FLIM LABS JSON imaging file to read.
232
+ The file name usually contains the string "_imaging" or "_phasor".
233
+ channel : int, optional
234
+ Index of channel to return.
235
+ By default, return the first channel.
236
+ If None, return all channels.
237
+ dtype : dtype_like, optional, default: uint16
238
+ Unsigned integer type of TCSPC histogram.
239
+ Increase the bit-depth for high photon counts.
240
+
241
+ Returns
242
+ -------
243
+ xarray.DataArray
244
+ TCSPC histogram with :ref:`axes codes <axes>` depending on
245
+ `channel` parameter:
246
+
247
+ - Single channel: axes ``'YXH'``
248
+ - Multiple channels: axes ``'CYXH'``
249
+
250
+ Type specified by ``dtype`` parameter.
251
+
252
+ - ``coords['H']``: delay-times of histogram bins in ns.
253
+ - ``coords['C']``: channel indices (if multiple channels).
254
+ - ``attrs['frequency']``: laser repetition frequency in MHz.
255
+ - ``attrs['flimlabs_header']``: FLIM LABS file header.
256
+
257
+ Raises
258
+ ------
259
+ ValueError
260
+ If file is not a valid JSON file.
261
+ If file is not a FLIM LABS JSON file containing TCSPC histogram.
262
+ If `dtype` is not an unsigned integer type.
263
+ IndexError
264
+ If `channel` is out of range.
265
+
266
+ See Also
267
+ --------
268
+ phasorpy.io.phasor_from_flimlabs_json
269
+
270
+ Examples
271
+ --------
272
+ >>> signal = signal_from_flimlabs_json(
273
+ ... fetch('Convallaria_m2_1740751781_phasor_ch1.json')
274
+ ... )
275
+ >>> signal.values
276
+ array(...)
277
+ >>> signal.shape
278
+ (256, 256, 256)
279
+ >>> signal.dims
280
+ ('Y', 'X', 'H')
281
+ >>> signal.coords['H'].data
282
+ array(...)
283
+ >>> signal.attrs['frequency'] # doctest: +NUMBER
284
+ 40.0
285
+
286
+ """
287
+ with open(filename, 'rb') as fh:
288
+ try:
289
+ data = json.load(fh)
290
+ except Exception as exc:
291
+ raise ValueError('not a valid JSON file') from exc
292
+
293
+ if (
294
+ 'header' not in data
295
+ or 'laser_period_ns' not in data['header']
296
+ or 'file_id' not in data['header']
297
+ or ('data' not in data and 'intensities_data' not in data)
298
+ ):
299
+ raise ValueError(
300
+ 'not a FLIM LABS JSON file containing TCSPC histogram'
301
+ )
302
+
303
+ if dtype is None:
304
+ dtype = numpy.uint16
305
+ else:
306
+ dtype = numpy.dtype(dtype)
307
+ if dtype.kind != 'u':
308
+ raise ValueError(f'{dtype=} is not an unsigned integer type')
309
+
310
+ header = data['header']
311
+ nchannels = len([c for c in header['channels'] if c])
312
+ height = header['image_height']
313
+ width = header['image_width']
314
+ frequency = 1000.0 / header['laser_period_ns']
315
+
316
+ if channel is not None:
317
+ if channel >= nchannels or channel < 0:
318
+ raise IndexError(f'{channel=} out of range[0, {nchannels=}]')
319
+ nchannels = 1
320
+
321
+ if 'data' in data:
322
+ # file_id = [73, 77, 71, 49] # 'IMG1'
323
+ intensities_data = data['data']
324
+ else:
325
+ # file_id = [73, 80, 71, 49] # 'IPG1'
326
+ intensities_data = data['intensities_data']
327
+
328
+ from .._phasorpy import _flimlabs_signal
329
+
330
+ signal = numpy.zeros((nchannels, height * width, 256), dtype)
331
+ _flimlabs_signal(
332
+ signal,
333
+ intensities_data,
334
+ -1 if channel is None else channel,
335
+ )
336
+
337
+ if channel is None and nchannels > 1:
338
+ signal.shape = (nchannels, height, width, 256)
339
+ axes = 'CYXH'
340
+ else:
341
+ signal.shape = (height, width, 256)
342
+ axes = 'YXH'
343
+
344
+ coords: dict[str, Any] = {}
345
+ coords['H'] = numpy.linspace(
346
+ 0.0, header['laser_period_ns'], 256, endpoint=False
347
+ )
348
+ if channel is None and nchannels > 1:
349
+ coords['C'] = numpy.asarray(
350
+ [i for i, c in enumerate(header['channels']) if c]
351
+ )
352
+
353
+ metadata = xarray_metadata(axes, signal.shape, filename, **coords)
354
+ attrs = metadata['attrs']
355
+ attrs['frequency'] = frequency
356
+ attrs['flimlabs_header'] = header
357
+
358
+ from xarray import DataArray
359
+
360
+ return DataArray(signal, **metadata)