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.

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