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.

415 lines
14 KiB

9 years ago
9 years ago
7 years ago
  1. import re
  2. from nd2reader.common import read_chunk, read_array, read_metadata, parse_date
  3. import xmltodict
  4. import six
  5. import numpy as np
  6. def ignore_missing(func):
  7. def wrapper(*args, **kwargs):
  8. try:
  9. return func(*args, **kwargs)
  10. except:
  11. return None
  12. return wrapper
  13. class RawMetadata(object):
  14. def __init__(self, fh, label_map):
  15. self._fh = fh
  16. self._label_map = label_map
  17. self._metadata_parsed = None
  18. @property
  19. def __dict__(self):
  20. """
  21. Returns the parsed metadata in dictionary form
  22. :return:
  23. """
  24. return self.get_parsed_metadata()
  25. def get_parsed_metadata(self):
  26. """
  27. Returns the parsed metadata in dictionary form
  28. :return:
  29. """
  30. if self._metadata_parsed is not None:
  31. return self._metadata_parsed
  32. self._metadata_parsed = {
  33. "height": self.image_attributes[six.b('SLxImageAttributes')][six.b('uiHeight')],
  34. "width": self.image_attributes[six.b('SLxImageAttributes')][six.b('uiWidth')],
  35. "date": parse_date(self.image_text_info[six.b('SLxImageTextInfo')]),
  36. "fields_of_view": self._parse_fields_of_view(),
  37. "frames": self._parse_frames(),
  38. "z_levels": self._parse_z_levels(),
  39. "total_images_per_channel": self._parse_total_images_per_channel(),
  40. "channels": self._parse_channels(),
  41. "pixel_microns": self.image_calibration.get(six.b('SLxCalibration'), {}).get(six.b('dCalibration')),
  42. }
  43. self._metadata_parsed['num_frames'] = len(self._metadata_parsed['frames'])
  44. self._parse_roi_metadata()
  45. self._parse_experiment_metadata()
  46. return self._metadata_parsed
  47. def _parse_channels(self):
  48. """
  49. These are labels created by the NIS Elements user. Typically they may a short description of the filter cube
  50. used (e.g. "bright field", "GFP", etc.)
  51. :rtype: list
  52. """
  53. channels = []
  54. metadata = self.image_metadata_sequence[six.b('SLxPictureMetadata')][six.b('sPicturePlanes')]
  55. try:
  56. validity = self.image_metadata[six.b('SLxExperiment')][six.b('ppNextLevelEx')][six.b('')][0][
  57. six.b('ppNextLevelEx')][six.b('')][0][six.b('pItemValid')]
  58. except (KeyError, TypeError):
  59. # If none of the channels have been deleted, there is no validity list, so we just make one
  60. validity = [True for _ in metadata]
  61. # Channel information is contained in dictionaries with the keys a0, a1...an where the number
  62. # indicates the order in which the channel is stored. So by sorting the dicts alphabetically
  63. # we get the correct order.
  64. for (label, chan), valid in zip(sorted(metadata[six.b('sPlaneNew')].items()), validity):
  65. if not valid:
  66. continue
  67. channels.append(chan[six.b('sDescription')].decode("utf8"))
  68. return channels
  69. def _parse_fields_of_view(self):
  70. """
  71. The metadata contains information about fields of view, but it contains it even if some fields
  72. of view were cropped. We can't find anything that states which fields of view are actually
  73. in the image data, so we have to calculate it. There probably is something somewhere, since
  74. NIS Elements can figure it out, but we haven't found it yet.
  75. :rtype: list
  76. """
  77. return self._parse_dimension(r""".*?XY\((\d+)\).*?""")
  78. def _parse_frames(self):
  79. """
  80. The number of cycles.
  81. :rtype: list
  82. """
  83. return self._parse_dimension(r""".*?T'?\((\d+)\).*?""")
  84. def _parse_z_levels(self):
  85. """
  86. The different levels in the Z-plane. Just a sequence from 0 to n.
  87. :rtype: list
  88. """
  89. return self._parse_dimension(r""".*?Z\((\d+)\).*?""")
  90. def _parse_dimension_text(self):
  91. """
  92. While there are metadata values that represent a lot of what we want to capture, they seem to be unreliable.
  93. Sometimes certain elements don't exist, or change their data type randomly. However, the human-readable text
  94. is always there and in the same exact format, so we just parse that instead.
  95. :rtype: str
  96. """
  97. dimension_text = six.b("")
  98. textinfo = self.image_text_info[six.b('SLxImageTextInfo')].values()
  99. for line in textinfo:
  100. if six.b("Dimensions:") in line:
  101. entries = line.split(six.b("\r\n"))
  102. for entry in entries:
  103. if entry.startswith(six.b("Dimensions:")):
  104. return entry
  105. return dimension_text
  106. def _parse_dimension(self, pattern):
  107. """
  108. :param pattern: a valid regex pattern
  109. :type pattern: str
  110. :rtype: list of int
  111. """
  112. dimension_text = self._parse_dimension_text()
  113. if six.PY3:
  114. dimension_text = dimension_text.decode("utf8")
  115. match = re.match(pattern, dimension_text)
  116. if not match:
  117. return [0]
  118. count = int(match.group(1))
  119. return list(range(count))
  120. def _parse_total_images_per_channel(self):
  121. """
  122. The total number of images per channel. Warning: this may be inaccurate as it includes "gap" images.
  123. :rtype: int
  124. """
  125. return self.image_attributes[six.b('SLxImageAttributes')][six.b('uiSequenceCount')]
  126. def _parse_roi_metadata(self):
  127. """
  128. Parse the raw ROI metadata.
  129. :return:
  130. """
  131. if self.roi_metadata is None or not six.b('RoiMetadata_v1') in self.roi_metadata:
  132. return
  133. raw_roi_data = self.roi_metadata[six.b('RoiMetadata_v1')]
  134. number_of_rois = raw_roi_data[six.b('m_vectGlobal_Size')]
  135. roi_objects = []
  136. for i in range(number_of_rois):
  137. current_roi = raw_roi_data[six.b('m_vectGlobal_%d' % i)]
  138. roi_objects.append(self._parse_roi(current_roi))
  139. self._metadata_parsed['rois'] = roi_objects
  140. def _parse_roi(self, raw_roi_dict):
  141. """
  142. Extract the vector animation parameters from the ROI.
  143. This includes the position and size at the given timepoints.
  144. :param raw_roi_dict:
  145. :return:
  146. """
  147. number_of_timepoints = raw_roi_dict[six.b('m_vectAnimParams_Size')]
  148. roi_dict = {
  149. "timepoints": [],
  150. "positions": [],
  151. "sizes": [],
  152. "shape": self._parse_roi_shape(raw_roi_dict[six.b('m_sInfo')][six.b('m_uiShapeType')]),
  153. "type": self._parse_roi_type(raw_roi_dict[six.b('m_sInfo')][six.b('m_uiInterpType')])
  154. }
  155. for i in range(number_of_timepoints):
  156. roi_dict = self._parse_vect_anim(roi_dict, raw_roi_dict[six.b('m_vectAnimParams_%d' % i)])
  157. # convert to NumPy arrays
  158. roi_dict["timepoints"] = np.array(roi_dict["timepoints"], dtype=np.float)
  159. roi_dict["positions"] = np.array(roi_dict["positions"], dtype=np.float)
  160. roi_dict["sizes"] = np.array(roi_dict["sizes"], dtype=np.float)
  161. return roi_dict
  162. @staticmethod
  163. def _parse_roi_shape(shape):
  164. if shape == 3:
  165. return 'rectangle'
  166. elif shape == 9:
  167. return 'circle'
  168. return None
  169. @staticmethod
  170. def _parse_roi_type(type_no):
  171. if type_no == 4:
  172. return 'stimulation'
  173. elif type_no == 3:
  174. return 'reference'
  175. elif type_no == 2:
  176. return 'background'
  177. return None
  178. def _parse_vect_anim(self, roi_dict, animation_dict):
  179. """
  180. Parses a ROI vector animation object and adds it to the global list of timepoints and positions.
  181. :param animation_dict:
  182. :return:
  183. """
  184. roi_dict["timepoints"].append(animation_dict[six.b('m_dTimeMs')])
  185. image_width = self._metadata_parsed["width"] * self._metadata_parsed["pixel_microns"]
  186. image_height = self._metadata_parsed["height"] * self._metadata_parsed["pixel_microns"]
  187. # positions are taken from the center of the image as a fraction of the half width/height of the image
  188. position = np.array((0.5 * image_width * (1 + animation_dict[six.b('m_dCenterX')]),
  189. 0.5 * image_height * (1 + animation_dict[six.b('m_dCenterY')]),
  190. animation_dict[six.b('m_dCenterZ')]))
  191. roi_dict["positions"].append(position)
  192. size_dict = animation_dict[six.b('m_sBoxShape')]
  193. # sizes are fractions of the half width/height of the image
  194. roi_dict["sizes"].append((size_dict[six.b('m_dSizeX')] * 0.25 * image_width,
  195. size_dict[six.b('m_dSizeY')] * 0.25 * image_height,
  196. size_dict[six.b('m_dSizeZ')]))
  197. return roi_dict
  198. def _parse_experiment_metadata(self):
  199. """
  200. Parse the metadata of the ND experiment
  201. :return:
  202. """
  203. if not six.b('SLxExperiment') in self.image_metadata:
  204. return
  205. raw_data = self.image_metadata[six.b('SLxExperiment')]
  206. experimental_data = {
  207. 'description': 'unknown',
  208. 'loops': []
  209. }
  210. if six.b('wsApplicationDesc') in raw_data:
  211. experimental_data['description'] = raw_data[six.b('wsApplicationDesc')].decode('utf8')
  212. if six.b('uLoopPars') in raw_data:
  213. experimental_data['loops'] = self._parse_loop_data(raw_data[six.b('uLoopPars')])
  214. self._metadata_parsed['experiment'] = experimental_data
  215. def _parse_loop_data(self, loop_data):
  216. """
  217. Parse the experimental loop data
  218. :param loop_data:
  219. :return:
  220. """
  221. if six.b('uiPeriodCount') not in loop_data or loop_data[six.b('uiPeriodCount')] == 0:
  222. return []
  223. if six.b('pPeriod') not in loop_data:
  224. return []
  225. # take the first dictionary element, it contains all loop data
  226. loops = loop_data[six.b('pPeriod')][list(loop_data[six.b('pPeriod')].keys())[0]]
  227. # take into account the absolute time in ms
  228. time_offset = 0
  229. parsed_loops = []
  230. for loop in loops:
  231. # duration of this loop
  232. duration = loop[six.b('dDuration')]
  233. # uiLoopType == 6 is a stimulation loop
  234. is_stimulation = loop[six.b('uiLoopType')] == 6
  235. # sampling interval in ms
  236. interval = loop[six.b('dAvgPeriodDiff')]
  237. parsed_loop = {
  238. 'start': time_offset,
  239. 'duration': duration,
  240. 'stimulation': is_stimulation,
  241. 'sampling_interval': interval
  242. }
  243. parsed_loops.append(parsed_loop)
  244. # increase the time offset
  245. time_offset += duration
  246. return parsed_loops
  247. @property
  248. @ignore_missing
  249. def image_text_info(self):
  250. return read_metadata(read_chunk(self._fh, self._label_map.image_text_info), 1)
  251. @property
  252. @ignore_missing
  253. def image_metadata_sequence(self):
  254. return read_metadata(read_chunk(self._fh, self._label_map.image_metadata_sequence), 1)
  255. @property
  256. @ignore_missing
  257. def image_calibration(self):
  258. return read_metadata(read_chunk(self._fh, self._label_map.image_calibration), 1)
  259. @property
  260. @ignore_missing
  261. def image_attributes(self):
  262. return read_metadata(read_chunk(self._fh, self._label_map.image_attributes), 1)
  263. @property
  264. @ignore_missing
  265. def x_data(self):
  266. return read_array(self._fh, 'double', self._label_map.x_data)
  267. @property
  268. @ignore_missing
  269. def y_data(self):
  270. return read_array(self._fh, 'double', self._label_map.y_data)
  271. @property
  272. @ignore_missing
  273. def z_data(self):
  274. return read_array(self._fh, 'double', self._label_map.z_data)
  275. @property
  276. @ignore_missing
  277. def roi_metadata(self):
  278. return read_metadata(read_chunk(self._fh, self._label_map.roi_metadata), 1)
  279. @property
  280. @ignore_missing
  281. def pfs_status(self):
  282. return read_array(self._fh, 'int', self._label_map.pfs_status)
  283. @property
  284. @ignore_missing
  285. def pfs_offset(self):
  286. return read_array(self._fh, 'int', self._label_map.pfs_offset)
  287. @property
  288. @ignore_missing
  289. def camera_exposure_time(self):
  290. return read_array(self._fh, 'double', self._label_map.camera_exposure_time)
  291. @property
  292. @ignore_missing
  293. def lut_data(self):
  294. return xmltodict.parse(read_chunk(self._fh, self._label_map.lut_data))
  295. @property
  296. @ignore_missing
  297. def grabber_settings(self):
  298. return xmltodict.parse(read_chunk(self._fh, self._label_map.grabber_settings))
  299. @property
  300. @ignore_missing
  301. def custom_data(self):
  302. return xmltodict.parse(read_chunk(self._fh, self._label_map.custom_data))
  303. @property
  304. @ignore_missing
  305. def app_info(self):
  306. return xmltodict.parse(read_chunk(self._fh, self._label_map.app_info))
  307. @property
  308. @ignore_missing
  309. def camera_temp(self):
  310. camera_temp = read_array(self._fh, 'double', self._label_map.camera_temp)
  311. if camera_temp:
  312. for temp in map(lambda x: round(x * 100.0, 2), camera_temp):
  313. yield temp
  314. @property
  315. @ignore_missing
  316. def acquisition_times(self):
  317. acquisition_times = read_array(self._fh, 'double', self._label_map.acquisition_times)
  318. if acquisition_times:
  319. for acquisition_time in map(lambda x: x / 1000.0, acquisition_times):
  320. yield acquisition_time
  321. @property
  322. @ignore_missing
  323. def image_metadata(self):
  324. if self._label_map.image_metadata:
  325. return read_metadata(read_chunk(self._fh, self._label_map.image_metadata), 1)