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.

349 lines
12 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
  1. # -*- coding: utf-8 -*-
  2. from datetime import datetime
  3. from nd2reader.model.metadata import Metadata, CameraSettings
  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. self._fh = fh
  112. self._metadata = None
  113. self._raw_metadata = None
  114. self._label_map = None
  115. self._camera_metadata = {}
  116. self._parse_metadata()
  117. @property
  118. def metadata(self):
  119. """
  120. :rtype: Metadata
  121. """
  122. return self._metadata
  123. @property
  124. def camera_metadata(self):
  125. return self._camera_metadata
  126. @property
  127. def driver(self):
  128. return V3Driver(self.metadata, self._label_map, self._fh)
  129. @property
  130. def raw_metadata(self):
  131. if not self._raw_metadata:
  132. self._label_map = self._build_label_map()
  133. self._raw_metadata = V3RawMetadata(self._fh, self._label_map)
  134. return self._raw_metadata
  135. def _parse_metadata(self):
  136. """
  137. Reads all metadata and instantiates the Metadata object.
  138. """
  139. height = self.raw_metadata.image_attributes[six.b('SLxImageAttributes')][six.b('uiHeight')]
  140. width = self.raw_metadata.image_attributes[six.b('SLxImageAttributes')][six.b('uiWidth')]
  141. date = self._parse_date(self.raw_metadata)
  142. fields_of_view = self._parse_fields_of_view(self.raw_metadata)
  143. frames = self._parse_frames(self.raw_metadata)
  144. z_levels = self._parse_z_levels(self.raw_metadata)
  145. total_images_per_channel = self._parse_total_images_per_channel(self.raw_metadata)
  146. channels = []
  147. for camera_setting in self._parse_camera_settings():
  148. channels.append(camera_setting.channel_name)
  149. self._camera_metadata[camera_setting.channel_name] = camera_setting
  150. self._metadata = Metadata(height, width, sorted(list(channels)), date, fields_of_view, frames, z_levels, total_images_per_channel)
  151. def _parse_camera_settings(self):
  152. for camera in self._raw_metadata.image_metadata_sequence[six.b('SLxPictureMetadata')][six.b('sPicturePlanes')][six.b('sSampleSetting')].values():
  153. name = camera[six.b('pCameraSetting')][six.b('CameraUserName')]
  154. id = camera[six.b('pCameraSetting')][six.b('CameraUniqueName')]
  155. exposure = camera[six.b('dExposureTime')]
  156. x_binning = camera[six.b('pCameraSetting')][six.b('FormatFast')][six.b('fmtDesc')][six.b('dBinningX')]
  157. y_binning = camera[six.b('pCameraSetting')][six.b('FormatFast')][six.b('fmtDesc')][six.b('dBinningY')]
  158. optical_configs = camera[six.b('sOpticalConfigs')]
  159. if six.b('') in optical_configs.keys():
  160. channel_name = optical_configs[six.b('')][six.b('sOpticalConfigName')]
  161. else:
  162. channel_name = None
  163. yield CameraSettings(name, id, exposure, x_binning, y_binning, channel_name)
  164. def _parse_date(self, raw_metadata):
  165. """
  166. The date and time when acquisition began.
  167. :type raw_metadata: V3RawMetadata
  168. :rtype: datetime.datetime() or None
  169. """
  170. for line in raw_metadata.image_text_info[six.b('SLxImageTextInfo')].values():
  171. line = line.decode("utf8")
  172. absolute_start_12 = None
  173. absolute_start_24 = None
  174. # ND2s seem to randomly switch between 12- and 24-hour representations.
  175. try:
  176. absolute_start_24 = datetime.strptime(line, "%m/%d/%Y %H:%M:%S")
  177. except (TypeError, ValueError):
  178. pass
  179. try:
  180. absolute_start_12 = datetime.strptime(line, "%m/%d/%Y %I:%M:%S %p")
  181. except (TypeError, ValueError):
  182. pass
  183. if not absolute_start_12 and not absolute_start_24:
  184. continue
  185. return absolute_start_12 if absolute_start_12 else absolute_start_24
  186. return None
  187. def _parse_channels(self, raw_metadata):
  188. """
  189. These are labels created by the NIS Elements user. Typically they may a short description of the filter cube
  190. used (e.g. "bright field", "GFP", etc.)
  191. :type raw_metadata: V3RawMetadata
  192. :rtype: list
  193. """
  194. channels = []
  195. metadata = raw_metadata.image_metadata_sequence[six.b('SLxPictureMetadata')][six.b('sPicturePlanes')]
  196. try:
  197. validity = raw_metadata.image_metadata[six.b('SLxExperiment')][six.b('ppNextLevelEx')][six.b('')][0][six.b('ppNextLevelEx')][six.b('')][0][six.b('pItemValid')]
  198. except (KeyError, TypeError):
  199. # If none of the channels have been deleted, there is no validity list, so we just make one
  200. validity = [True for _ in metadata]
  201. # Channel information is contained in dictionaries with the keys a0, a1...an where the number
  202. # indicates the order in which the channel is stored. So by sorting the dicts alphabetically
  203. # we get the correct order.
  204. for (label, chan), valid in zip(sorted(metadata[six.b('sPlaneNew')].items()), validity):
  205. if not valid:
  206. continue
  207. channels.append(chan[six.b('sDescription')].decode("utf8"))
  208. return channels
  209. def _parse_fields_of_view(self, raw_metadata):
  210. """
  211. The metadata contains information about fields of view, but it contains it even if some fields
  212. of view were cropped. We can't find anything that states which fields of view are actually
  213. in the image data, so we have to calculate it. There probably is something somewhere, since
  214. NIS Elements can figure it out, but we haven't found it yet.
  215. :type raw_metadata: V3RawMetadata
  216. :rtype: list
  217. """
  218. return self._parse_dimension(r""".*?XY\((\d+)\).*?""", raw_metadata)
  219. def _parse_frames(self, raw_metadata):
  220. """
  221. The number of cycles.
  222. :type raw_metadata: V3RawMetadata
  223. :rtype: list
  224. """
  225. return self._parse_dimension(r""".*?T'?\((\d+)\).*?""", raw_metadata)
  226. def _parse_z_levels(self, raw_metadata):
  227. """
  228. The different levels in the Z-plane. Just a sequence from 0 to n.
  229. :type raw_metadata: V3RawMetadata
  230. :rtype: list
  231. """
  232. return self._parse_dimension(r""".*?Z\((\d+)\).*?""", raw_metadata)
  233. def _parse_dimension_text(self, raw_metadata):
  234. """
  235. While there are metadata values that represent a lot of what we want to capture, they seem to be unreliable.
  236. Sometimes certain elements don't exist, or change their data type randomly. However, the human-readable text
  237. is always there and in the same exact format, so we just parse that instead.
  238. :type raw_metadata: V3RawMetadata
  239. :rtype: str
  240. """
  241. for line in raw_metadata.image_text_info[six.b('SLxImageTextInfo')].values():
  242. if six.b("Dimensions:") in line:
  243. metadata = line
  244. break
  245. else:
  246. return six.b("")
  247. for line in metadata.split(six.b("\r\n")):
  248. if line.startswith(six.b("Dimensions:")):
  249. dimension_text = line
  250. break
  251. else:
  252. return six.b("")
  253. return dimension_text
  254. def _parse_dimension(self, pattern, raw_metadata):
  255. """
  256. :param pattern: a valid regex pattern
  257. :type pattern: str
  258. :type raw_metadata: V3RawMetadata
  259. :rtype: list of int
  260. """
  261. dimension_text = self._parse_dimension_text(raw_metadata)
  262. if six.PY3:
  263. dimension_text = dimension_text.decode("utf8")
  264. match = re.match(pattern, dimension_text)
  265. if not match:
  266. return [0]
  267. count = int(match.group(1))
  268. return list(range(count))
  269. def _parse_total_images_per_channel(self, raw_metadata):
  270. """
  271. The total number of images per channel. Warning: this may be inaccurate as it includes "gap" images.
  272. :type raw_metadata: V3RawMetadata
  273. :rtype: int
  274. """
  275. return raw_metadata.image_attributes[six.b('SLxImageAttributes')][six.b('uiSequenceCount')]
  276. def _build_label_map(self):
  277. """
  278. Every label ends with an exclamation point, however, we can't directly search for those to find all the labels
  279. as some of the bytes contain the value 33, which is the ASCII code for "!". So we iteratively find each label,
  280. grab the subsequent data (always 16 bytes long), advance to the next label and repeat.
  281. :rtype: LabelMap
  282. """
  283. self._fh.seek(-8, 2)
  284. chunk_map_start_location = struct.unpack("Q", self._fh.read(8))[0]
  285. self._fh.seek(chunk_map_start_location)
  286. raw_text = self._fh.read(-1)
  287. return LabelMap(raw_text)