phasorpy 0.5__cp313-cp313-win_arm64.whl → 0.6__cp313-cp313-win_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- phasorpy/__init__.py +2 -3
- phasorpy/_phasorpy.cp313-win_arm64.pyd +0 -0
- phasorpy/_phasorpy.pyx +185 -2
- phasorpy/_utils.py +121 -9
- phasorpy/cli.py +56 -3
- phasorpy/cluster.py +42 -6
- phasorpy/components.py +226 -55
- phasorpy/experimental.py +312 -0
- phasorpy/io/__init__.py +137 -0
- phasorpy/io/_flimlabs.py +350 -0
- phasorpy/io/_leica.py +329 -0
- phasorpy/io/_ometiff.py +445 -0
- phasorpy/io/_other.py +782 -0
- phasorpy/io/_simfcs.py +627 -0
- phasorpy/phasor.py +307 -1
- phasorpy/plot/__init__.py +27 -0
- phasorpy/plot/_functions.py +717 -0
- phasorpy/plot/_lifetime_plots.py +553 -0
- phasorpy/plot/_phasorplot.py +1119 -0
- phasorpy/plot/_phasorplot_fret.py +559 -0
- phasorpy/utils.py +84 -296
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/METADATA +2 -2
- phasorpy-0.6.dist-info/RECORD +34 -0
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/WHEEL +1 -1
- phasorpy/_io.py +0 -2655
- phasorpy/io.py +0 -9
- phasorpy/plot.py +0 -2318
- phasorpy/version.py +0 -80
- phasorpy-0.5.dist-info/RECORD +0 -26
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/licenses/LICENSE.txt +0 -0
- {phasorpy-0.5.dist-info → phasorpy-0.6.dist-info}/top_level.txt +0 -0
phasorpy/io/_ometiff.py
ADDED
@@ -0,0 +1,445 @@
|
|
1
|
+
"""Read and write OME-TIFF file format."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
__all__ = [
|
6
|
+
# 'signal_from_ometiff',
|
7
|
+
'phasor_from_ometiff',
|
8
|
+
'phasor_to_ometiff',
|
9
|
+
]
|
10
|
+
|
11
|
+
import os
|
12
|
+
import re
|
13
|
+
from typing import TYPE_CHECKING
|
14
|
+
|
15
|
+
from .._utils import parse_harmonic
|
16
|
+
from ..utils import logger
|
17
|
+
|
18
|
+
if TYPE_CHECKING:
|
19
|
+
from .._typing import (
|
20
|
+
Any,
|
21
|
+
ArrayLike,
|
22
|
+
DTypeLike,
|
23
|
+
Literal,
|
24
|
+
NDArray,
|
25
|
+
PathLike,
|
26
|
+
Sequence,
|
27
|
+
)
|
28
|
+
|
29
|
+
import numpy
|
30
|
+
|
31
|
+
from .. import __version__
|
32
|
+
|
33
|
+
|
34
|
+
def phasor_to_ometiff(
|
35
|
+
filename: str | PathLike[Any],
|
36
|
+
mean: ArrayLike,
|
37
|
+
real: ArrayLike,
|
38
|
+
imag: ArrayLike,
|
39
|
+
/,
|
40
|
+
*,
|
41
|
+
frequency: float | None = None,
|
42
|
+
harmonic: int | Sequence[int] | None = None,
|
43
|
+
dims: Sequence[str] | None = None,
|
44
|
+
dtype: DTypeLike | None = None,
|
45
|
+
description: str | None = None,
|
46
|
+
**kwargs: Any,
|
47
|
+
) -> None:
|
48
|
+
"""Write phasor coordinate images and metadata to OME-TIFF file.
|
49
|
+
|
50
|
+
The OME-TIFF format is compatible with Bio-Formats and Fiji.
|
51
|
+
|
52
|
+
By default, write phasor coordinates as single precision floating point
|
53
|
+
values to separate image series.
|
54
|
+
Write images larger than (1024, 1024) pixels as (256, 256) tiles, datasets
|
55
|
+
larger than 2 GB as BigTIFF, and datasets larger than 8 KB using
|
56
|
+
zlib compression.
|
57
|
+
|
58
|
+
This file format is experimental and might be incompatible with future
|
59
|
+
versions of this library. It is intended for temporarily exchanging
|
60
|
+
phasor coordinates with other software, not as a long-term storage
|
61
|
+
solution.
|
62
|
+
|
63
|
+
Parameters
|
64
|
+
----------
|
65
|
+
filename : str or Path
|
66
|
+
Name of PhasorPy OME-TIFF file to write.
|
67
|
+
mean : array_like
|
68
|
+
Average intensity image. Write to an image series named 'Phasor mean'.
|
69
|
+
real : array_like
|
70
|
+
Image of real component of phasor coordinates.
|
71
|
+
Multiple harmonics, if any, must be in the first dimension.
|
72
|
+
Write to image series named 'Phasor real'.
|
73
|
+
imag : array_like
|
74
|
+
Image of imaginary component of phasor coordinates.
|
75
|
+
Multiple harmonics, if any, must be in the first dimension.
|
76
|
+
Write to image series named 'Phasor imag'.
|
77
|
+
frequency : float, optional
|
78
|
+
Fundamental frequency of time-resolved phasor coordinates.
|
79
|
+
Usually in units of MHz.
|
80
|
+
Write to image series named 'Phasor frequency'.
|
81
|
+
harmonic : int or sequence of int, optional
|
82
|
+
Harmonics present in the first dimension of `real` and `imag`, if any.
|
83
|
+
Write to image series named 'Phasor harmonic'.
|
84
|
+
It is only needed if harmonics are not starting at and increasing by
|
85
|
+
one.
|
86
|
+
dims : sequence of str, optional
|
87
|
+
Character codes for `mean` image dimensions.
|
88
|
+
By default, the last dimensions are assumed to be 'TZCYX'.
|
89
|
+
If harmonics are present in `real` and `imag`, an "other" (``Q``)
|
90
|
+
dimension is prepended to axes for those arrays.
|
91
|
+
Refer to the OME-TIFF model for allowed axes and their order.
|
92
|
+
dtype : dtype-like, optional
|
93
|
+
Floating point data type used to store phasor coordinates.
|
94
|
+
The default is ``float32``, which has 6 digits of precision
|
95
|
+
and maximizes compatibility with other software.
|
96
|
+
description : str, optional
|
97
|
+
Plain-text description of dataset. Write as OME dataset description.
|
98
|
+
**kwargs
|
99
|
+
Additional arguments passed to :py:class:`tifffile.TiffWriter` and
|
100
|
+
:py:meth:`tifffile.TiffWriter.write`.
|
101
|
+
For example, ``compression=None`` writes image data uncompressed.
|
102
|
+
|
103
|
+
See Also
|
104
|
+
--------
|
105
|
+
phasorpy.io.phasor_from_ometiff
|
106
|
+
|
107
|
+
Notes
|
108
|
+
-----
|
109
|
+
Scalar or one-dimensional phasor coordinate arrays are written as images.
|
110
|
+
|
111
|
+
The OME-TIFF format is specified in the
|
112
|
+
`OME Data Model and File Formats Documentation
|
113
|
+
<https://ome-model.readthedocs.io/>`_.
|
114
|
+
|
115
|
+
The `6D, 7D and 8D storage
|
116
|
+
<https://ome-model.readthedocs.io/en/latest/developers/6d-7d-and-8d-storage.html>`_
|
117
|
+
extension is used to store multi-harmonic phasor coordinates.
|
118
|
+
The modulo type for the first, harmonic dimension is "other".
|
119
|
+
|
120
|
+
The implementation is based on the
|
121
|
+
`tifffile <https://github.com/cgohlke/tifffile/>`__ library.
|
122
|
+
|
123
|
+
Examples
|
124
|
+
--------
|
125
|
+
>>> mean, real, imag = numpy.random.rand(3, 32, 32, 32)
|
126
|
+
>>> phasor_to_ometiff(
|
127
|
+
... '_phasorpy.ome.tif', mean, real, imag, dims='ZYX', frequency=80.0
|
128
|
+
... )
|
129
|
+
|
130
|
+
"""
|
131
|
+
import tifffile
|
132
|
+
|
133
|
+
if dtype is None:
|
134
|
+
dtype = numpy.float32
|
135
|
+
dtype = numpy.dtype(dtype)
|
136
|
+
if dtype.kind != 'f':
|
137
|
+
raise ValueError(f'{dtype=} not a floating point type')
|
138
|
+
|
139
|
+
mean = numpy.asarray(mean, dtype)
|
140
|
+
real = numpy.asarray(real, dtype)
|
141
|
+
imag = numpy.asarray(imag, dtype)
|
142
|
+
datasize = mean.nbytes + real.nbytes + imag.nbytes
|
143
|
+
|
144
|
+
if real.shape != imag.shape:
|
145
|
+
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
146
|
+
if mean.shape != real.shape[-mean.ndim :]:
|
147
|
+
raise ValueError(f'{mean.shape=} != {real.shape[-mean.ndim:]=}')
|
148
|
+
has_harmonic_dim = real.ndim == mean.ndim + 1
|
149
|
+
if mean.ndim == real.ndim or real.ndim == 0:
|
150
|
+
nharmonic = 1
|
151
|
+
else:
|
152
|
+
nharmonic = real.shape[0]
|
153
|
+
|
154
|
+
if mean.ndim < 2:
|
155
|
+
# not an image
|
156
|
+
mean = mean.reshape(1, -1)
|
157
|
+
if has_harmonic_dim:
|
158
|
+
real = real.reshape(real.shape[0], 1, -1)
|
159
|
+
imag = imag.reshape(imag.shape[0], 1, -1)
|
160
|
+
else:
|
161
|
+
real = real.reshape(1, -1)
|
162
|
+
imag = imag.reshape(1, -1)
|
163
|
+
|
164
|
+
if harmonic is not None:
|
165
|
+
harmonic, _ = parse_harmonic(harmonic)
|
166
|
+
if len(harmonic) != nharmonic:
|
167
|
+
raise ValueError('invalid harmonic')
|
168
|
+
|
169
|
+
if frequency is not None:
|
170
|
+
frequency_array = numpy.atleast_2d(frequency).astype(numpy.float64)
|
171
|
+
if frequency_array.size > 1:
|
172
|
+
raise ValueError('frequency must be scalar')
|
173
|
+
|
174
|
+
axes = 'TZCYX'[-mean.ndim :] if dims is None else ''.join(tuple(dims))
|
175
|
+
if len(axes) != mean.ndim:
|
176
|
+
raise ValueError(f'{axes=} does not match {mean.ndim=}')
|
177
|
+
axes_phasor = axes if mean.ndim == real.ndim else 'Q' + axes
|
178
|
+
|
179
|
+
if 'photometric' not in kwargs:
|
180
|
+
kwargs['photometric'] = 'minisblack'
|
181
|
+
if 'compression' not in kwargs and datasize > 8192:
|
182
|
+
kwargs['compression'] = 'zlib'
|
183
|
+
if 'tile' not in kwargs and 'rowsperstrip' not in kwargs:
|
184
|
+
if (
|
185
|
+
axes.endswith('YX')
|
186
|
+
and mean.shape[-1] > 1024
|
187
|
+
and mean.shape[-2] > 1024
|
188
|
+
):
|
189
|
+
kwargs['tile'] = (256, 256)
|
190
|
+
|
191
|
+
mode = kwargs.pop('mode', None)
|
192
|
+
bigtiff = kwargs.pop('bigtiff', None)
|
193
|
+
if bigtiff is None:
|
194
|
+
bigtiff = datasize > 2**31
|
195
|
+
|
196
|
+
metadata = kwargs.pop('metadata', {})
|
197
|
+
if 'Creator' not in metadata:
|
198
|
+
metadata['Creator'] = f'PhasorPy {__version__}'
|
199
|
+
|
200
|
+
dataset = metadata.pop('Dataset', {})
|
201
|
+
if 'Name' not in dataset:
|
202
|
+
dataset['Name'] = 'Phasor'
|
203
|
+
if description:
|
204
|
+
dataset['Description'] = description
|
205
|
+
metadata['Dataset'] = dataset
|
206
|
+
|
207
|
+
if has_harmonic_dim:
|
208
|
+
metadata['TypeDescription'] = {'Q': 'Phasor harmonics'}
|
209
|
+
|
210
|
+
with tifffile.TiffWriter(
|
211
|
+
filename, bigtiff=bigtiff, mode=mode, ome=True
|
212
|
+
) as tif:
|
213
|
+
metadata['Name'] = 'Phasor mean'
|
214
|
+
metadata['axes'] = axes
|
215
|
+
tif.write(mean, metadata=metadata, **kwargs)
|
216
|
+
del metadata['Dataset']
|
217
|
+
|
218
|
+
metadata['Name'] = 'Phasor real'
|
219
|
+
metadata['axes'] = axes_phasor
|
220
|
+
tif.write(real, metadata=metadata, **kwargs)
|
221
|
+
|
222
|
+
metadata['Name'] = 'Phasor imag'
|
223
|
+
tif.write(imag, metadata=metadata, **kwargs)
|
224
|
+
|
225
|
+
if frequency is not None:
|
226
|
+
tif.write(frequency_array, metadata={'Name': 'Phasor frequency'})
|
227
|
+
|
228
|
+
if harmonic is not None:
|
229
|
+
tif.write(
|
230
|
+
numpy.atleast_2d(harmonic).astype(numpy.uint32),
|
231
|
+
metadata={'Name': 'Phasor harmonic'},
|
232
|
+
)
|
233
|
+
|
234
|
+
|
235
|
+
def phasor_from_ometiff(
|
236
|
+
filename: str | PathLike[Any],
|
237
|
+
/,
|
238
|
+
*,
|
239
|
+
harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
|
240
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any], dict[str, Any]]:
|
241
|
+
"""Return phasor coordinates and metadata from PhasorPy OME-TIFF file.
|
242
|
+
|
243
|
+
PhasorPy OME-TIFF files contain phasor mean intensity, real and imaginary
|
244
|
+
components, along with frequency and harmonic information.
|
245
|
+
|
246
|
+
Parameters
|
247
|
+
----------
|
248
|
+
filename : str or Path
|
249
|
+
Name of PhasorPy OME-TIFF file to read.
|
250
|
+
harmonic : int, sequence of int, or 'all', optional
|
251
|
+
Harmonic(s) to return from file.
|
252
|
+
If None (default), return the first harmonic stored in the file.
|
253
|
+
If `'all'`, return all harmonics as stored in file.
|
254
|
+
If a list, the first axes of the returned `real` and `imag` arrays
|
255
|
+
contain specified harmonic(s).
|
256
|
+
If an integer, the returned `real` and `imag` arrays are single
|
257
|
+
harmonic and have the same shape as `mean`.
|
258
|
+
|
259
|
+
Returns
|
260
|
+
-------
|
261
|
+
mean : ndarray
|
262
|
+
Average intensity image.
|
263
|
+
real : ndarray
|
264
|
+
Image of real component of phasor coordinates.
|
265
|
+
imag : ndarray
|
266
|
+
Image of imaginary component of phasor coordinates.
|
267
|
+
attrs : dict
|
268
|
+
Select metadata:
|
269
|
+
|
270
|
+
- ``'dims'`` (tuple of str):
|
271
|
+
:ref:`Axes codes <axes>` for `mean` image dimensions.
|
272
|
+
- ``'harmonic'`` (int or list of int):
|
273
|
+
Harmonic(s) present in `real` and `imag`.
|
274
|
+
If a scalar, `real` and `imag` are single harmonic and contain no
|
275
|
+
harmonic axes.
|
276
|
+
If a list, `real` and `imag` contain one or more harmonics in the
|
277
|
+
first axis.
|
278
|
+
- ``'frequency'`` (float, optional):
|
279
|
+
Fundamental frequency of time-resolved phasor coordinates.
|
280
|
+
Usually in units of MHz.
|
281
|
+
- ``'description'`` (str, optional):
|
282
|
+
OME dataset plain-text description.
|
283
|
+
|
284
|
+
Raises
|
285
|
+
------
|
286
|
+
tifffile.TiffFileError
|
287
|
+
File is not a TIFF file.
|
288
|
+
ValueError
|
289
|
+
File is not an OME-TIFF containing phasor coordinates.
|
290
|
+
IndexError
|
291
|
+
Harmonic is not found in file.
|
292
|
+
|
293
|
+
See Also
|
294
|
+
--------
|
295
|
+
phasorpy.io.phasor_to_ometiff
|
296
|
+
|
297
|
+
Notes
|
298
|
+
-----
|
299
|
+
Scalar or one-dimensional phasor coordinates stored in the file are
|
300
|
+
returned as two-dimensional images (three-dimensional if multiple
|
301
|
+
harmonics are present).
|
302
|
+
|
303
|
+
The implementation is based on the
|
304
|
+
`tifffile <https://github.com/cgohlke/tifffile/>`__ library.
|
305
|
+
|
306
|
+
Examples
|
307
|
+
--------
|
308
|
+
>>> mean, real, imag = numpy.random.rand(3, 32, 32, 32)
|
309
|
+
>>> phasor_to_ometiff(
|
310
|
+
... '_phasorpy.ome.tif', mean, real, imag, dims='ZYX', frequency=80.0
|
311
|
+
... )
|
312
|
+
>>> mean, real, imag, attrs = phasor_from_ometiff('_phasorpy.ome.tif')
|
313
|
+
>>> mean
|
314
|
+
array(...)
|
315
|
+
>>> mean.dtype
|
316
|
+
dtype('float32')
|
317
|
+
>>> mean.shape
|
318
|
+
(32, 32, 32)
|
319
|
+
>>> attrs['dims']
|
320
|
+
('Z', 'Y', 'X')
|
321
|
+
>>> attrs['frequency']
|
322
|
+
80.0
|
323
|
+
>>> attrs['harmonic']
|
324
|
+
1
|
325
|
+
|
326
|
+
"""
|
327
|
+
import tifffile
|
328
|
+
|
329
|
+
name = os.path.basename(filename)
|
330
|
+
|
331
|
+
with tifffile.TiffFile(filename) as tif:
|
332
|
+
if (
|
333
|
+
not tif.is_ome
|
334
|
+
or len(tif.series) < 3
|
335
|
+
or tif.series[0].name != 'Phasor mean'
|
336
|
+
or tif.series[1].name != 'Phasor real'
|
337
|
+
or tif.series[2].name != 'Phasor imag'
|
338
|
+
):
|
339
|
+
raise ValueError(
|
340
|
+
f'{name!r} is not an OME-TIFF containing phasor images'
|
341
|
+
)
|
342
|
+
|
343
|
+
attrs: dict[str, Any] = {'dims': tuple(tif.series[0].axes)}
|
344
|
+
|
345
|
+
# TODO: read coords from OME-XML
|
346
|
+
ome_xml = tif.ome_metadata
|
347
|
+
assert ome_xml is not None
|
348
|
+
|
349
|
+
# TODO: parse OME-XML
|
350
|
+
match = re.search(
|
351
|
+
r'><Description>(.*)</Description><',
|
352
|
+
ome_xml,
|
353
|
+
re.MULTILINE | re.DOTALL,
|
354
|
+
)
|
355
|
+
if match is not None:
|
356
|
+
attrs['description'] = (
|
357
|
+
match.group(1)
|
358
|
+
.replace('&', '&')
|
359
|
+
.replace('>', '>')
|
360
|
+
.replace('<', '<')
|
361
|
+
)
|
362
|
+
|
363
|
+
has_harmonic_dim = tif.series[1].ndim > tif.series[0].ndim
|
364
|
+
nharmonics = tif.series[1].shape[0] if has_harmonic_dim else 1
|
365
|
+
harmonic_max = nharmonics
|
366
|
+
for i in (3, 4):
|
367
|
+
if len(tif.series) < i + 1:
|
368
|
+
break
|
369
|
+
series = tif.series[i]
|
370
|
+
data = series.asarray().squeeze()
|
371
|
+
if series.name == 'Phasor frequency':
|
372
|
+
attrs['frequency'] = float(data.item(0))
|
373
|
+
elif series.name == 'Phasor harmonic':
|
374
|
+
if not has_harmonic_dim and data.size == 1:
|
375
|
+
attrs['harmonic'] = int(data.item(0))
|
376
|
+
harmonic_max = attrs['harmonic']
|
377
|
+
elif has_harmonic_dim and data.size == nharmonics:
|
378
|
+
attrs['harmonic'] = data.tolist()
|
379
|
+
harmonic_max = max(attrs['harmonic'])
|
380
|
+
else:
|
381
|
+
logger().warning(
|
382
|
+
f'harmonic={data} does not match phasor '
|
383
|
+
f'shape={tif.series[1].shape}'
|
384
|
+
)
|
385
|
+
|
386
|
+
if 'harmonic' not in attrs:
|
387
|
+
if has_harmonic_dim:
|
388
|
+
attrs['harmonic'] = list(range(1, nharmonics + 1))
|
389
|
+
else:
|
390
|
+
attrs['harmonic'] = 1
|
391
|
+
harmonic_stored = attrs['harmonic']
|
392
|
+
|
393
|
+
mean = tif.series[0].asarray()
|
394
|
+
if harmonic is None:
|
395
|
+
# first harmonic in file
|
396
|
+
if isinstance(harmonic_stored, list):
|
397
|
+
attrs['harmonic'] = harmonic_stored[0]
|
398
|
+
else:
|
399
|
+
attrs['harmonic'] = harmonic_stored
|
400
|
+
real = tif.series[1].asarray()
|
401
|
+
if has_harmonic_dim:
|
402
|
+
real = real[0].copy()
|
403
|
+
imag = tif.series[2].asarray()
|
404
|
+
if has_harmonic_dim:
|
405
|
+
imag = imag[0].copy()
|
406
|
+
elif isinstance(harmonic, str) and harmonic == 'all':
|
407
|
+
# all harmonics as stored in file
|
408
|
+
real = tif.series[1].asarray()
|
409
|
+
imag = tif.series[2].asarray()
|
410
|
+
else:
|
411
|
+
# specified harmonics
|
412
|
+
harmonic, keepdims = parse_harmonic(harmonic, harmonic_max)
|
413
|
+
try:
|
414
|
+
if isinstance(harmonic_stored, list):
|
415
|
+
index = [harmonic_stored.index(h) for h in harmonic]
|
416
|
+
else:
|
417
|
+
index = [[harmonic_stored].index(h) for h in harmonic]
|
418
|
+
except ValueError as exc:
|
419
|
+
raise IndexError('harmonic not found') from exc
|
420
|
+
|
421
|
+
if has_harmonic_dim:
|
422
|
+
if keepdims:
|
423
|
+
attrs['harmonic'] = [harmonic_stored[i] for i in index]
|
424
|
+
real = tif.series[1].asarray()[index].copy()
|
425
|
+
imag = tif.series[2].asarray()[index].copy()
|
426
|
+
else:
|
427
|
+
attrs['harmonic'] = harmonic_stored[index[0]]
|
428
|
+
real = tif.series[1].asarray()[index[0]].copy()
|
429
|
+
imag = tif.series[2].asarray()[index[0]].copy()
|
430
|
+
elif keepdims:
|
431
|
+
real = tif.series[1].asarray()
|
432
|
+
real = real.reshape(1, *real.shape)
|
433
|
+
imag = tif.series[2].asarray()
|
434
|
+
imag = imag.reshape(1, *imag.shape)
|
435
|
+
attrs['harmonic'] = [harmonic_stored]
|
436
|
+
else:
|
437
|
+
real = tif.series[1].asarray()
|
438
|
+
imag = tif.series[2].asarray()
|
439
|
+
|
440
|
+
if real.shape != imag.shape:
|
441
|
+
logger().warning(f'{real.shape=} != {imag.shape=}')
|
442
|
+
if real.shape[-mean.ndim :] != mean.shape:
|
443
|
+
logger().warning(f'{real.shape[-mean.ndim:]=} != {mean.shape=}')
|
444
|
+
|
445
|
+
return mean, real, imag, attrs
|