phasorpy 0.1__cp312-cp312-win_amd64.whl → 0.3__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.
phasorpy/io.py CHANGED
@@ -16,6 +16,7 @@ The ``phasorpy.io`` module provides functions to:
16
16
  - read time-resolved and hyperspectral image data and metadata (as relevant
17
17
  to phasor analysis) from many file formats used in bio-imaging:
18
18
 
19
+ - :py:func:`read_imspector_tiff` - ImSpector FLIM TIFF
19
20
  - :py:func:`read_lsm` - Zeiss LSM
20
21
  - :py:func:`read_ifli` - ISS IFLI
21
22
  - :py:func:`read_sdt` - Becker & Hickl SDT
@@ -123,6 +124,7 @@ __all__ = [
123
124
  'read_fbd',
124
125
  'read_flif',
125
126
  'read_ifli',
127
+ 'read_imspector_tiff',
126
128
  # 'read_lif',
127
129
  'read_lsm',
128
130
  # 'read_nd2',
@@ -290,11 +292,9 @@ def phasor_to_ometiff(
290
292
  imag = imag.reshape(1, -1)
291
293
 
292
294
  if harmonic is not None:
293
- harmonic_array = numpy.atleast_1d(harmonic)
294
- if harmonic_array.ndim > 1 or harmonic_array.size != nharmonic:
295
+ harmonic, _ = parse_harmonic(harmonic)
296
+ if len(harmonic) != nharmonic:
295
297
  raise ValueError('invalid harmonic')
296
- samples = int(harmonic_array.max()) * 2 + 1
297
- harmonic, _ = parse_harmonic(harmonic, samples)
298
298
 
299
299
  if frequency is not None:
300
300
  frequency_array = numpy.atleast_2d(frequency).astype(numpy.float64)
@@ -488,7 +488,7 @@ def phasor_from_ometiff(
488
488
 
489
489
  has_harmonic_dim = tif.series[1].ndim > tif.series[0].ndim
490
490
  nharmonics = tif.series[1].shape[0] if has_harmonic_dim else 1
491
- maxharmonic = nharmonics
491
+ harmonic_max = nharmonics
492
492
  for i in (3, 4):
493
493
  if len(tif.series) < i + 1:
494
494
  break
@@ -499,10 +499,10 @@ def phasor_from_ometiff(
499
499
  elif series.name == 'Phasor harmonic':
500
500
  if not has_harmonic_dim and data.size == 1:
501
501
  attrs['harmonic'] = int(data.item(0))
502
- maxharmonic = attrs['harmonic']
502
+ harmonic_max = attrs['harmonic']
503
503
  elif has_harmonic_dim and data.size == nharmonics:
504
504
  attrs['harmonic'] = data.tolist()
505
- maxharmonic = max(attrs['harmonic'])
505
+ harmonic_max = max(attrs['harmonic'])
506
506
  else:
507
507
  logger.warning(
508
508
  f'harmonic={data} does not match phasor '
@@ -535,7 +535,7 @@ def phasor_from_ometiff(
535
535
  imag = tif.series[2].asarray()
536
536
  else:
537
537
  # specified harmonics
538
- harmonic, keepdims = parse_harmonic(harmonic, 2 * maxharmonic + 1)
538
+ harmonic, keepdims = parse_harmonic(harmonic, harmonic_max)
539
539
  try:
540
540
  if isinstance(harmonic_stored, list):
541
541
  index = [harmonic_stored.index(h) for h in harmonic]
@@ -769,7 +769,7 @@ def phasor_from_simfcs_referenced(
769
769
  else:
770
770
  raise ValueError(f'file extension must be .ref or .r64, not {ext!r}')
771
771
 
772
- harmonic, keep_harmonic_dim = parse_harmonic(harmonic, data.shape[0])
772
+ harmonic, keep_harmonic_dim = parse_harmonic(harmonic, data.shape[0] // 2)
773
773
 
774
774
  mean = data[0].copy()
775
775
  real = numpy.empty((len(harmonic),) + mean.shape, numpy.float32)
@@ -889,6 +889,146 @@ def read_lsm(
889
889
  return DataArray(data, **metadata)
890
890
 
891
891
 
892
+ def read_imspector_tiff(
893
+ filename: str | PathLike[Any],
894
+ /,
895
+ ) -> DataArray:
896
+ """Return FLIM image stack and metadata from ImSpector TIFF file.
897
+
898
+ Parameters
899
+ ----------
900
+ filename : str or Path
901
+ Name of ImSpector FLIM TIFF file to read.
902
+
903
+ Returns
904
+ -------
905
+ xarray.DataArray
906
+ TCSPC image stack.
907
+ Usually, a 3-to-5-dimensional array of type ``uint16``.
908
+
909
+ - ``coords['H']``: times of histogram bins.
910
+ - ``attrs['frequency']``: repetition frequency in MHz.
911
+
912
+ Raises
913
+ ------
914
+ tifffile.TiffFileError
915
+ File is not a TIFF file.
916
+ ValueError
917
+ File is not an ImSpector FLIM TIFF file.
918
+
919
+ Examples
920
+ --------
921
+ >>> data = read_imspector_tiff(fetch('Embryo.tif'))
922
+ >>> data.values
923
+ array(...)
924
+ >>> data.dtype
925
+ dtype('uint16')
926
+ >>> data.shape
927
+ (56, 512, 512)
928
+ >>> data.dims
929
+ ('H', 'Y', 'X')
930
+ >>> data.coords['H'].data # dtime bins
931
+ array(...)
932
+ >>> data.attrs['frequency'] # doctest: +NUMBER
933
+ 80.109
934
+
935
+ """
936
+ from xml.etree import ElementTree
937
+
938
+ import tifffile
939
+
940
+ with tifffile.TiffFile(filename) as tif:
941
+ tags = tif.pages.first.tags
942
+ omexml = tags.valueof(270, '')
943
+ make = tags.valueof(271, '')
944
+
945
+ if (
946
+ make != 'ImSpector'
947
+ or not omexml.startswith('<?xml version')
948
+ or len(tif.series) != 1
949
+ or not tif.is_ome
950
+ ):
951
+ raise ValueError(f'{tif.filename} is not an ImSpector TIFF file')
952
+
953
+ series = tif.series[0]
954
+ ndim = series.ndim
955
+ axes = series.axes
956
+ shape = series.shape
957
+
958
+ if ndim < 3 or not axes.endswith('YX'):
959
+ raise ValueError(
960
+ f'{tif.filename} is not an ImSpector FLIM TIFF file'
961
+ )
962
+
963
+ data = series.asarray()
964
+
965
+ attrs: dict[str, Any] = {}
966
+ coords = {}
967
+ physical_size = {}
968
+
969
+ root = ElementTree.fromstring(omexml)
970
+ ns = {
971
+ '': 'http://www.openmicroscopy.org/Schemas/OME/2008-02',
972
+ 'ca': 'http://www.openmicroscopy.org/Schemas/CA/2008-02',
973
+ }
974
+
975
+ description = root.find('.//Description', ns)
976
+ if (
977
+ description is not None
978
+ and description.text
979
+ and description.text != 'not_specified'
980
+ ):
981
+ attrs['description'] = description.text
982
+
983
+ pixels = root.find('.//Image/Pixels', ns)
984
+ assert pixels is not None
985
+ for ax in 'TZYX':
986
+ attrib = 'TimeIncrement' if ax == 'T' else f'PhysicalSize{ax}'
987
+ if ax not in axes or attrib not in pixels.attrib:
988
+ continue
989
+ size = float(pixels.attrib[attrib])
990
+ physical_size[ax] = size
991
+ coords[ax] = numpy.linspace(
992
+ 0.0,
993
+ size,
994
+ shape[axes.index(ax)],
995
+ endpoint=False,
996
+ dtype=numpy.float64,
997
+ )
998
+
999
+ axes_labels = root.find('.//ca:CustomAttributes/AxesLabels', ns)
1000
+ if (
1001
+ axes_labels is None
1002
+ or 'X' not in axes_labels.attrib
1003
+ or 'TCSPC' not in axes_labels.attrib['X']
1004
+ or 'FirstAxis' not in axes_labels.attrib
1005
+ or 'SecondAxis' not in axes_labels.attrib
1006
+ ):
1007
+ raise ValueError(f'{tif.filename} is not an ImSpector FLIM TIFF file')
1008
+
1009
+ if axes_labels.attrib['FirstAxis'].endswith('TCSPC T'):
1010
+ ax = axes[-3]
1011
+ assert axes_labels.attrib['FirstAxis-Unit'] == 'ns'
1012
+ elif axes_labels.attrib['SecondAxis'].endswith('TCSPC T') and ndim > 3:
1013
+ ax = axes[-4]
1014
+ assert axes_labels.attrib['SecondAxis-Unit'] == 'ns'
1015
+ else:
1016
+ raise ValueError(f'{tif.filename} is not an ImSpector FLIM TIFF file')
1017
+ axes = axes.replace(ax, 'H')
1018
+ coords['H'] = coords[ax]
1019
+ del coords[ax]
1020
+
1021
+ attrs['frequency'] = float(
1022
+ 1000.0 / (shape[axes.index('H')] * physical_size[ax])
1023
+ )
1024
+
1025
+ metadata = _metadata(axes, shape, filename, attrs=attrs, **coords)
1026
+
1027
+ from xarray import DataArray
1028
+
1029
+ return DataArray(data, **metadata)
1030
+
1031
+
892
1032
  def read_ifli(
893
1033
  filename: str | PathLike[Any],
894
1034
  /,
@@ -944,8 +1084,8 @@ def read_ifli(
944
1084
  (256, 256, 4, 3)
945
1085
  >>> data.dims
946
1086
  ('Y', 'X', 'F', 'S')
947
- >>> data.coords['F'].data
948
- array([8.033...])
1087
+ >>> data.coords['F'].data # doctest: +NUMBER
1088
+ array([8.033e+07, 1.607e+08, 2.41e+08, 4.017e+08])
949
1089
  >>> data.coords['S'].data
950
1090
  array(['mean', 'real', 'imag'], dtype='<U4')
951
1091
  >>> data.attrs
@@ -1037,8 +1177,8 @@ def read_sdt(
1037
1177
  ('Y', 'X', 'H')
1038
1178
  >>> data.coords['H'].data
1039
1179
  array(...)
1040
- >>> data.attrs['frequency']
1041
- 79...
1180
+ >>> data.attrs['frequency'] # doctest: +NUMBER
1181
+ 79.99
1042
1182
 
1043
1183
  """
1044
1184
  import sdtfile
@@ -1150,7 +1290,7 @@ def read_ptu(
1150
1290
  ('T', 'Y', 'X', 'C', 'H')
1151
1291
  >>> data.coords['H'].data
1152
1292
  array(...)
1153
- >>> data.attrs['frequency']
1293
+ >>> data.attrs['frequency'] # doctest: +NUMBER
1154
1294
  78.02
1155
1295
 
1156
1296
  """
@@ -1223,8 +1363,8 @@ def read_flif(
1223
1363
  ('H', 'Y', 'X')
1224
1364
  >>> data.coords['H'].data
1225
1365
  array(...)
1226
- >>> data.attrs['frequency']
1227
- 80.65...
1366
+ >>> data.attrs['frequency'] # doctest: +NUMBER
1367
+ 80.65
1228
1368
 
1229
1369
  """
1230
1370
  import lfdfiles