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.

533 lines
17 KiB

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