You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

319 lines
11 KiB

9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
  1. # -*- coding: utf-8 -*-
  2. from datetime import datetime
  3. from nd2reader.model.metadata import Metadata
  4. from nd2reader.model.label import LabelMap
  5. from nd2reader.parser.base import BaseParser
  6. from nd2reader.driver.v3 import V3Driver
  7. from nd2reader.common.v3 import read_chunk, read_array, read_metadata
  8. import re
  9. import six
  10. import struct
  11. import xmltodict
  12. def ignore_missing(func):
  13. def wrapper(*args, **kwargs):
  14. try:
  15. return func(*args, **kwargs)
  16. except:
  17. return None
  18. return wrapper
  19. class V3RawMetadata(object):
  20. def __init__(self, fh, label_map):
  21. self._fh = fh
  22. self._label_map = label_map
  23. @property
  24. @ignore_missing
  25. def image_text_info(self):
  26. return read_metadata(read_chunk(self._fh, self._label_map.image_text_info), 1)
  27. @property
  28. @ignore_missing
  29. def image_metadata_sequence(self):
  30. return read_metadata(read_chunk(self._fh, self._label_map.image_metadata_sequence), 1)
  31. @property
  32. @ignore_missing
  33. def image_calibration(self):
  34. return read_metadata(read_chunk(self._fh, self._label_map.image_calibration), 1)
  35. @property
  36. @ignore_missing
  37. def image_attributes(self):
  38. return read_metadata(read_chunk(self._fh, self._label_map.image_attributes), 1)
  39. @property
  40. @ignore_missing
  41. def x_data(self):
  42. return read_array(self._fh, 'double', self._label_map.x_data)
  43. @property
  44. @ignore_missing
  45. def y_data(self):
  46. return read_array(self._fh, 'double', self._label_map.y_data)
  47. @property
  48. @ignore_missing
  49. def z_data(self):
  50. return read_array(self._fh, 'double', self._label_map.z_data)
  51. @property
  52. @ignore_missing
  53. def roi_metadata(self):
  54. return read_metadata(read_chunk(self._fh, self._label_map.roi_metadata), 1)
  55. @property
  56. @ignore_missing
  57. def pfs_status(self):
  58. return read_array(self._fh, 'int', self._label_map.pfs_status)
  59. @property
  60. @ignore_missing
  61. def pfs_offset(self):
  62. return read_array(self._fh, 'int', self._label_map.pfs_offset)
  63. @property
  64. @ignore_missing
  65. def camera_exposure_time(self):
  66. return read_array(self._fh, 'double', self._label_map.camera_exposure_time)
  67. @property
  68. @ignore_missing
  69. def lut_data(self):
  70. return xmltodict.parse(read_chunk(self._fh, self._label_map.lut_data))
  71. @property
  72. @ignore_missing
  73. def grabber_settings(self):
  74. return xmltodict.parse(read_chunk(self._fh, self._label_map.grabber_settings))
  75. @property
  76. @ignore_missing
  77. def custom_data(self):
  78. return xmltodict.parse(read_chunk(self._fh, self._label_map.custom_data))
  79. @property
  80. @ignore_missing
  81. def app_info(self):
  82. return xmltodict.parse(read_chunk(self._fh, self._label_map.app_info))
  83. @property
  84. @ignore_missing
  85. def camera_temp(self):
  86. camera_temp = read_array(self._fh, 'double', self._label_map.camera_temp)
  87. if camera_temp:
  88. for temp in map(lambda x: round(x * 100.0, 2), camera_temp):
  89. yield temp
  90. @property
  91. @ignore_missing
  92. def acquisition_times(self):
  93. acquisition_times = read_array(self._fh, 'double', self._label_map.acquisition_times)
  94. if acquisition_times:
  95. for acquisition_time in map(lambda x: x / 1000.0, acquisition_times):
  96. yield acquisition_time
  97. @property
  98. @ignore_missing
  99. def image_metadata(self):
  100. if self._label_map.image_metadata:
  101. return read_metadata(read_chunk(self._fh, self._label_map.image_metadata), 1)
  102. class V3Parser(BaseParser):
  103. """ Parses ND2 files and creates a Metadata and driver object. """
  104. CHUNK_HEADER = 0xabeceda
  105. CHUNK_MAP_START = six.b("ND2 FILEMAP SIGNATURE NAME 0001!")
  106. CHUNK_MAP_END = six.b("ND2 CHUNK MAP SIGNATURE 0000001!")
  107. def __init__(self, fh):
  108. """
  109. :type fh: file
  110. """
  111. if six.PY3:
  112. super().__init__(fh)
  113. else:
  114. super(V3Parser, self).__init__(fh)
  115. self._label_map = self._build_label_map()
  116. self.raw_metadata = V3RawMetadata(self._fh, self._label_map)
  117. self._parse_metadata()
  118. @property
  119. def driver(self):
  120. """
  121. Provides an object that knows how to look up and read images based on an index.
  122. """
  123. return V3Driver(self.metadata, self._label_map, self._fh)
  124. def _parse_metadata(self):
  125. """
  126. Reads all metadata and instantiates the Metadata object.
  127. """
  128. height = self.raw_metadata.image_attributes[six.b('SLxImageAttributes')][six.b('uiHeight')]
  129. width = self.raw_metadata.image_attributes[six.b('SLxImageAttributes')][six.b('uiWidth')]
  130. date = self._parse_date(self.raw_metadata)
  131. fields_of_view = self._parse_fields_of_view(self.raw_metadata)
  132. frames = self._parse_frames(self.raw_metadata)
  133. z_levels = self._parse_z_levels(self.raw_metadata)
  134. total_images_per_channel = self._parse_total_images_per_channel(self.raw_metadata)
  135. channels = self._parse_channels(self.raw_metadata)
  136. pixel_microns = self.raw_metadata.image_calibration.get(six.b('SLxCalibration'), {}).get(six.b('dCalibration'))
  137. self.metadata = Metadata(height, width, channels, date, fields_of_view, frames, z_levels, total_images_per_channel, pixel_microns)
  138. def _parse_date(self, raw_metadata):
  139. """
  140. The date and time when acquisition began.
  141. :type raw_metadata: V3RawMetadata
  142. :rtype: datetime.datetime() or None
  143. """
  144. for line in raw_metadata.image_text_info[six.b('SLxImageTextInfo')].values():
  145. line = line.decode("utf8")
  146. absolute_start_12 = None
  147. absolute_start_24 = None
  148. # ND2s seem to randomly switch between 12- and 24-hour representations.
  149. try:
  150. absolute_start_24 = datetime.strptime(line, "%m/%d/%Y %H:%M:%S")
  151. except (TypeError, ValueError):
  152. pass
  153. try:
  154. absolute_start_12 = datetime.strptime(line, "%m/%d/%Y %I:%M:%S %p")
  155. except (TypeError, ValueError):
  156. pass
  157. if not absolute_start_12 and not absolute_start_24:
  158. continue
  159. return absolute_start_12 if absolute_start_12 else absolute_start_24
  160. return None
  161. def _parse_channels(self, raw_metadata):
  162. """
  163. These are labels created by the NIS Elements user. Typically they may a short description of the filter cube
  164. used (e.g. "bright field", "GFP", etc.)
  165. :type raw_metadata: V3RawMetadata
  166. :rtype: list
  167. """
  168. channels = []
  169. metadata = raw_metadata.image_metadata_sequence[six.b('SLxPictureMetadata')][six.b('sPicturePlanes')]
  170. try:
  171. validity = raw_metadata.image_metadata[six.b('SLxExperiment')][six.b('ppNextLevelEx')][six.b('')][0][six.b('ppNextLevelEx')][six.b('')][0][six.b('pItemValid')]
  172. except (KeyError, TypeError):
  173. # If none of the channels have been deleted, there is no validity list, so we just make one
  174. validity = [True for _ in metadata]
  175. # Channel information is contained in dictionaries with the keys a0, a1...an where the number
  176. # indicates the order in which the channel is stored. So by sorting the dicts alphabetically
  177. # we get the correct order.
  178. for (label, chan), valid in zip(sorted(metadata[six.b('sPlaneNew')].items()), validity):
  179. if not valid:
  180. continue
  181. channels.append(chan[six.b('sDescription')].decode("utf8"))
  182. return channels
  183. def _parse_fields_of_view(self, raw_metadata):
  184. """
  185. The metadata contains information about fields of view, but it contains it even if some fields
  186. of view were cropped. We can't find anything that states which fields of view are actually
  187. in the image data, so we have to calculate it. There probably is something somewhere, since
  188. NIS Elements can figure it out, but we haven't found it yet.
  189. :type raw_metadata: V3RawMetadata
  190. :rtype: list
  191. """
  192. return self._parse_dimension(r""".*?XY\((\d+)\).*?""", raw_metadata)
  193. def _parse_frames(self, raw_metadata):
  194. """
  195. The number of cycles.
  196. :type raw_metadata: V3RawMetadata
  197. :rtype: list
  198. """
  199. return self._parse_dimension(r""".*?T'?\((\d+)\).*?""", raw_metadata)
  200. def _parse_z_levels(self, raw_metadata):
  201. """
  202. The different levels in the Z-plane. Just a sequence from 0 to n.
  203. :type raw_metadata: V3RawMetadata
  204. :rtype: list
  205. """
  206. return self._parse_dimension(r""".*?Z\((\d+)\).*?""", raw_metadata)
  207. def _parse_dimension_text(self, raw_metadata):
  208. """
  209. While there are metadata values that represent a lot of what we want to capture, they seem to be unreliable.
  210. Sometimes certain elements don't exist, or change their data type randomly. However, the human-readable text
  211. is always there and in the same exact format, so we just parse that instead.
  212. :type raw_metadata: V3RawMetadata
  213. :rtype: str
  214. """
  215. for line in raw_metadata.image_text_info[six.b('SLxImageTextInfo')].values():
  216. if six.b("Dimensions:") in line:
  217. metadata = line
  218. break
  219. else:
  220. return six.b("")
  221. for line in metadata.split(six.b("\r\n")):
  222. if line.startswith(six.b("Dimensions:")):
  223. dimension_text = line
  224. break
  225. else:
  226. return six.b("")
  227. return dimension_text
  228. def _parse_dimension(self, pattern, raw_metadata):
  229. """
  230. :param pattern: a valid regex pattern
  231. :type pattern: str
  232. :type raw_metadata: V3RawMetadata
  233. :rtype: list of int
  234. """
  235. dimension_text = self._parse_dimension_text(raw_metadata)
  236. if six.PY3:
  237. dimension_text = dimension_text.decode("utf8")
  238. match = re.match(pattern, dimension_text)
  239. if not match:
  240. return [0]
  241. count = int(match.group(1))
  242. return list(range(count))
  243. def _parse_total_images_per_channel(self, raw_metadata):
  244. """
  245. The total number of images per channel. Warning: this may be inaccurate as it includes "gap" images.
  246. :type raw_metadata: V3RawMetadata
  247. :rtype: int
  248. """
  249. return raw_metadata.image_attributes[six.b('SLxImageAttributes')][six.b('uiSequenceCount')]
  250. def _build_label_map(self):
  251. """
  252. Every label ends with an exclamation point, however, we can't directly search for those to find all the labels
  253. as some of the bytes contain the value 33, which is the ASCII code for "!". So we iteratively find each label,
  254. grab the subsequent data (always 16 bytes long), advance to the next label and repeat.
  255. :rtype: LabelMap
  256. """
  257. self._fh.seek(-8, 2)
  258. chunk_map_start_location = struct.unpack("Q", self._fh.read(8))[0]
  259. self._fh.seek(chunk_map_start_location)
  260. raw_text = self._fh.read(-1)
  261. return LabelMap(raw_text)