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.

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