io.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. import tifffile
  2. import numpy as np
  3. import yaml
  4. import pathlib as pl
  5. import datetime as dt
  6. import xml.etree.ElementTree as ET
  7. import logging
  8. import os
  9. def read_tif_2Dor3D(tif_file, flip_y=True, return_3D=False, load_data=True):
  10. """
  11. Read a TIF file into numpy array. TIF file axes are assumed to be TYX or YX
  12. :param str tif_file: path of tif file
  13. :param bool flip_y: whether to flip Y axis
  14. :param bool return_3D: whether to convert 2D to 3D if required
  15. :return: numpy.ndarray in XY or XYT format
  16. Read metadata and return as dictionary
  17. Tiff file could be OME
  18. e.g. Live Acquisition, or Fiji
  19. :param str tif_file: path of tif file
  20. :param bool flip_y: whether to flip Y axis
  21. return: numpy array XYT, metadata dictionary
  22. """
  23. # if tif_file is str, convert it to path
  24. if type(tif_file) == str:
  25. tif_file = pl.Path(tif_file)
  26. # load metadata
  27. # tif_file=animal_list[10]
  28. with tifffile.TiffFile(tif_file) as tif:
  29. metadata = tif.imagej_metadata
  30. # imagej_metadata does not work any more or never worked on stack - read metadata from first frame
  31. if metadata is None:
  32. metadata = tif.pages[0].description
  33. # extract XML tree from metadata into root
  34. try:
  35. root = ET.fromstring(metadata)
  36. metadata_present = True
  37. # define namespace for OME data
  38. # this uses xTree OME syntax
  39. # https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.Element
  40. ns = {
  41. "d": "http://www.openmicroscopy.org/Schemas/OME/2013-06"
  42. }
  43. # now get all infos that we put into settings file
  44. meta_info = root.find("./d:Image/d:Pixels", ns).attrib
  45. # result is a dictionary, for example:
  46. # {'ID': 'Pixels:1-0',
  47. # 'DimensionOrder': 'XYTZC',
  48. # 'Type': 'uint16',
  49. # 'SizeX': '1392',
  50. # 'SizeY': '1040',
  51. # 'SizeZ': '1',
  52. # 'SizeC': '1',
  53. # 'SizeT': '160',
  54. # 'PhysicalSizeX': '6.45',
  55. # 'PhysicalSizeY': '6.45',
  56. # 'PhysicalSizeZ': '1000',
  57. # 'SignificantBits': '14'}
  58. # acquisition date as string, e.g. '2021-09-19T16:49:28'
  59. AcquisitionDate = root.find("./d:Image/d:AcquisitionDate", ns).text
  60. meta_info.update({'AcquisitionDate':AcquisitionDate})
  61. # binning info, e.g. '1x1'
  62. Binning = root.find("./d:Image/d:Pixels/d:Channel/d:DetectorSettings", ns).attrib["Binning"]
  63. meta_info.update({'Binning':Binning})
  64. # frame interval
  65. # relative time of secoond image (first image looks unsafe - often it is blanck. Therefore use frames 2 and 3)
  66. time_frame1 = root.find("./d:Image/d:Pixels/d:Plane[2]", ns).attrib["DeltaT"]
  67. # relative time of third image
  68. time_frame2 = root.find("./d:Image/d:Pixels/d:Plane[3]", ns).attrib["DeltaT"]
  69. GDMfreq = (float(time_frame2) - float(time_frame1))
  70. GDMfreq = int(GDMfreq*1000 + 0.5) # unit is ms, rounded
  71. meta_info.update({'GDMfreq':str(GDMfreq)})
  72. # exposure time for frame 2 - expecting that to be uniform
  73. ExposureTime_ms = float(root.find("./d:Image/d:Pixels/d:Plane[2]", ns).attrib["ExposureTime"])
  74. ExposureTime_ms = int(1000*ExposureTime_ms) # value in Andor is in seconds
  75. meta_info.update({'ExposureTime_ms':str(ExposureTime_ms)})
  76. # columns in .settings that need to be filled here:
  77. # get the tif file, including the last directory
  78. this_filename = tif_file.parts
  79. dbb = this_filename[-2] +'/'+ this_filename[-1]
  80. meta_info.update({'dbb':dbb})
  81. meta_info.update({'Label':this_filename[-1]})
  82. # PxSzX
  83. # replace the Andor name "PhysicalSizeX' with the Galizia name PsSzX
  84. meta_info['PsSzX'] = meta_info.pop('PhysicalSizeX')
  85. meta_info['PsSzY'] = meta_info.pop('PhysicalSizeY')
  86. # PxSzY, e.g. 1.5625
  87. # When was this measurement taken?
  88. # first get the time when the measurement was started
  89. measurementtime = dt.datetime.fromisoformat(AcquisitionDate)
  90. # now add the time of the first frame, since measurement start time ie equal for all measurements in one loop
  91. measurementtime_delta = dt.timedelta(seconds=float(time_frame1))
  92. measurementtime = measurementtime + measurementtime_delta
  93. # StartTime, e.g. 10:05:04
  94. StartTime = measurementtime.strftime('%H:%M:%S')
  95. meta_info.update({'StartTime':StartTime})
  96. # UTC, e.g. 1623229504.482
  97. UTC = measurementtime.timestamp()
  98. meta_info.update({'UTCTime':UTC})
  99. except:
  100. metadata_present = False
  101. meta_info = None
  102. #load data
  103. if load_data:
  104. with tifffile.TiffFile(tif_file) as tif:
  105. imagej_hyperstack = tif.asarray()
  106. if len(imagej_hyperstack.shape) == 3: # 3D data in TYX format
  107. if flip_y:
  108. imagej_hyperstack = np.flip(imagej_hyperstack, axis=1)
  109. imagej_hyperstack = imagej_hyperstack.swapaxes(0, 2) # return in XYT format
  110. # with the Andor camera in Konstanz, run with LA software
  111. # the first image in a series is overexposed, we need to check for this
  112. # thus, remove first frame if it is outside 3*sd of the mean of the next 10 frames
  113. if np.std(imagej_hyperstack[:,:,0]) == 0: #all numbers in first frame are equal
  114. logging.getLogger("VIEW").info(f"Read_tif_2Dor3D in {os.path.basename(__file__)}: removed first frame because no information present")
  115. imagej_hyperstack = imagej_hyperstack[:,:,1:]
  116. if metadata_present:
  117. SizeT = str(int(meta_info['SizeT']) - 1 )
  118. meta_info.update({'SizeT':SizeT})
  119. # this only works if ALL pixels in the first frame are overexposed
  120. # therefore, additionally, if first frame is outside the noise
  121. # defined here as 2 times SD in the first 15 frames
  122. else: #check condition
  123. line = imagej_hyperstack.mean(axis=(0,1))
  124. condition = (line[0] > (np.mean(line[1:15]) + 2*np.std(line[1:15])))
  125. if condition:
  126. logging.getLogger("VIEW").info(f"Read_tif_2Dor3D in {os.path.basename(__file__)}: removed first frame because average value above noise")
  127. imagej_hyperstack = imagej_hyperstack[:,:,1:]
  128. if metadata_present:
  129. SizeT = str(int(meta_info['SizeT']) - 1 )
  130. meta_info.update({'SizeT':SizeT})
  131. # read 2D tif data
  132. elif len(imagej_hyperstack.shape) == 2: # 2D data in YX format
  133. if flip_y:
  134. imagej_hyperstack = np.flip(imagej_hyperstack, axis=0) # YX to XY format
  135. imagej_hyperstack = imagej_hyperstack.swapaxes(0, 1)
  136. if return_3D:
  137. imagej_hyperstack = np.stack([imagej_hyperstack], axis=2)
  138. else:
  139. imagej_hyperstack = None
  140. return imagej_hyperstack, meta_info
  141. # end read_ometif_metadict
  142. def read_single_file_fura_tif(tif_file):
  143. """
  144. Read FURA data from <tif_file>. Assume input file has the format TWYX, where W is wavelength and
  145. this dimension has size 2.
  146. :param str tif_file: absolute path of the file on file system
  147. :rtype: data_340, data_380
  148. data_340: 340nm data as an numpy.ndarray, format XYT
  149. data_380: 380nm data as an numpy.ndarray, format XYT
  150. """
  151. data_in = tifffile.imread(tif_file)
  152. data_in = np.flip(data_in, axis=1) # format TWYX
  153. # split data, each will have format TYX
  154. data_340 = data_in[:, 1, :, :]
  155. data_380 = data_in[:, 0, :, :]
  156. return data_340.swapaxes(0, 2), data_380.swapaxes(0, 2) # return in format XYT
  157. def write_tif_2Dor3D(array_xy_or_xyt, tif_file, dtype=None, scale_data=False, labels=None):
  158. """
  159. Write a 2D or a 3D numpy array to a TIFF file with data type format <dtype>. If <dtype> is None, data is written
  160. in its own data type. Else, the function will try to safely cast data in <array_xy_or_xyt> to <dtype>.
  161. If it is not possible and <scale_data> is True, data is scaled to fit the dynamic range of <dtype> and
  162. written to disc. Else an error is raised
  163. :param numpy.ndarray array_xy_or_xyt: array to be written
  164. :param str tif_file: name of file to which data is to be written
  165. :param dtype: data type format to use. Must be a valid numerical numpy dtype
  166. (https://numpy.org/doc/stable/reference/arrays.scalars.html)
  167. :param str|Sequence labels: a str or 1 member sequence when <array_xy_or_xyt> is 2D, else a sequence
  168. with the same size as the last (3rd) dimension of <array_xy_or_xyt>
  169. """
  170. if dtype is None:
  171. array_cast = array_xy_or_xyt
  172. else:
  173. if issubclass(dtype, np.integer):
  174. info = np.iinfo(dtype)
  175. elif issubclass(dtype, np.flexible):
  176. info = np.finfo(dtype)
  177. else:
  178. raise ValueError(
  179. "Invalid dtype. Please specify a valid numerical numpy dtype "
  180. "(https://numpy.org/doc/stable/reference/arrays.scalars.html)")
  181. if np.can_cast(array_xy_or_xyt, dtype):
  182. array_cast = array_xy_or_xyt.astype(dtype)
  183. elif scale_data:
  184. array_min, array_max = array_xy_or_xyt.min(), array_xy_or_xyt.max()
  185. array_xy_or_xyt_0_1 = (array_xy_or_xyt - array_min) / (array_max - array_min)
  186. array_scaled = info.min + array_xy_or_xyt_0_1 * (info.max - info.min)
  187. array_cast = array_scaled.astype(dtype)
  188. else:
  189. raise ValueError(
  190. f"The values in the specified array could not be safely cast into the specified dtype ({dtype})."
  191. f"If you want the values in the specified array to be scaled into the dynamic range of {dtype}, "
  192. f"set the argument <scale_data> to True")
  193. # flip Y axis
  194. array_cast = np.flip(array_cast, axis=1)
  195. if type(labels) is str:
  196. labels = [labels]
  197. if len(array_cast.shape) == 2:
  198. array_to_write = array_cast.swapaxes(0, 1) # from XY to YX
  199. if labels is not None:
  200. assert len(labels) == 1, \
  201. f"Expected one label to write along with a one page TIF. Got ({len(labels)})"
  202. elif len(array_cast.shape) == 3:
  203. array_to_write = array_cast.swapaxes(0, 2) # from XYT to TYX
  204. if labels is not None:
  205. assert len(labels) == array_cast.shape[2], \
  206. f"Expected {array_cast.shape[2]} labels two write along with array with shape {array_cast.shape}. " \
  207. f"Got {len(labels)}"
  208. else:
  209. raise ValueError("This function can only write 2D or 3D arrays")
  210. kwargs = {"description": None}
  211. if labels is not None:
  212. kwargs["description"] = "Labels=" + ";;".join(labels)
  213. tifffile.imwrite(file=tif_file, data=array_to_write, **kwargs)
  214. def read_check_yml_file(yml_filename, expected_type=None):
  215. """
  216. Reads flags from <filename>, applies some checks and returns them
  217. :param yml_filename: str, path of a .yml file
  218. :param expected_type: any, if specified, as assertion error is raised if contents
  219. of the yml file is not of the specfied type
  220. :return: any, depending of contents of the yml file
  221. """
  222. with open(yml_filename, 'r') as fle:
  223. yml_contents = yaml.load(fle, yaml.SafeLoader)
  224. if expected_type is not None:
  225. assert type(yml_contents) is expected_type, f"YML file {yml_filename} was expected to contain " \
  226. f"{expected_type} data," \
  227. f"found, {type(yml_contents)} instead"
  228. return yml_contents
  229. def write_yml(yml_filename, to_write):
  230. with open(yml_filename, 'w') as fle:
  231. yaml.dump(to_write, fle, Dumper=yaml.SafeDumper)
  232. def read_lsm(path):
  233. """ takes a path to a lsm file, reads the file with the tifffile lib and
  234. returns a np array
  235. """
  236. data_cut = tifffile.imread(path)
  237. data_cut_rot = np.swapaxes(data_cut, 0, 2)
  238. data_cut_rot_flip = np.flip(data_cut_rot, axis=1)
  239. return data_cut_rot_flip
  240. def load_pst(filename):
  241. """
  242. read tillvision based .pst files as uint16.
  243. """
  244. # filename can have an extension (e.g. .pst), or not
  245. # reading stack size from inf
  246. # inf_path = os.path.splitext(filename)[0] + '.inf'
  247. # this does not work for /data/030725bR.pst\\dbb10F, remove extension by hand,
  248. # assuming it is exactly 3 elements
  249. if filename.endswith(".pst") or filename.endswith(".ps"):
  250. filepath = pl.Path(filename)
  251. else:
  252. filepath = pl.Path(f"{filename}.pst")
  253. if not filepath.is_file():
  254. filepath = filepath.with_suffix(".ps")
  255. assert filepath.is_file(), \
  256. f"Could not find either of the following raw data files:\n{filename}.pst\n{filename}.ps"
  257. meta = {}
  258. with open(filepath.with_suffix(".inf"), 'r') as fh:
  259. # fh.next()
  260. for line in fh.readlines():
  261. try:
  262. k, v = line.strip().split('=')
  263. meta[k] = v
  264. except:
  265. pass
  266. # reading stack from pst
  267. shape = np.int32((meta['Width'], meta['Height'], meta['Frames']))
  268. expected_units = np.prod(shape)
  269. assert filepath.stat().st_size >= 2 * expected_units, \
  270. f"Expected at least {2 * expected_units} bytes in {filepath}. Found {filepath.stat().st_size}"
  271. raw = np.fromfile(filepath, dtype='int16', count=expected_units)
  272. data = np.reshape(raw, shape, order='F')
  273. # was swapping x, y axes; commented out to retain original order
  274. # data = data.swapaxes(0,1)
  275. # data is upside down as compared to what we see in TillVision
  276. data = np.flip(data, axis=1)
  277. data = data.astype('uint16')
  278. return data