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.

638 lines
20 KiB

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