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.

320 lines
13 KiB

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