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.

381 lines
15 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. # -*- coding: utf-8 -*-
  2. import array
  3. from datetime import datetime
  4. import numpy as np
  5. import re
  6. import struct
  7. import six
  8. class Nd2Parser(object):
  9. """
  10. Reads .nd2 files, provides an interface to the metadata, and generates numpy arrays from the image data.
  11. You should not ever need to instantiate this class manually unless you're a developer.
  12. """
  13. CHUNK_HEADER = 0xabeceda
  14. CHUNK_MAP_START = six.b("ND2 FILEMAP SIGNATURE NAME 0001!")
  15. CHUNK_MAP_END = six.b("ND2 CHUNK MAP SIGNATURE 0000001!")
  16. def __init__(self, filename):
  17. self._absolute_start = None
  18. self._filename = filename
  19. self._fh = None
  20. self._channels = None
  21. self._channel_count = None
  22. self._chunk_map_start_location = None
  23. self._cursor_position = 0
  24. self._dimension_text = None
  25. self._fields_of_view = None
  26. self._label_map = {}
  27. self.metadata = {}
  28. self._read_map()
  29. self._time_indexes = None
  30. self._parse_metadata()
  31. self._z_levels = None
  32. @property
  33. def absolute_start(self):
  34. """
  35. The date and time when acquisition began.
  36. :rtype: datetime.datetime()
  37. """
  38. if self._absolute_start is None:
  39. for line in self.metadata[six.b('ImageTextInfo')][six.b('SLxImageTextInfo')].values():
  40. line = line.decode("utf8")
  41. absolute_start_12 = None
  42. absolute_start_24 = None
  43. # ND2s seem to randomly switch between 12- and 24-hour representations.
  44. try:
  45. absolute_start_24 = datetime.strptime(line, "%m/%d/%Y %H:%M:%S")
  46. except (TypeError, ValueError):
  47. pass
  48. try:
  49. absolute_start_12 = datetime.strptime(line, "%m/%d/%Y %I:%M:%S %p")
  50. except (TypeError, ValueError):
  51. pass
  52. if not absolute_start_12 and not absolute_start_24:
  53. continue
  54. return absolute_start_12 if absolute_start_12 else absolute_start_24
  55. raise ValueError("This ND2 has no recorded start time. This is probably a bug.")
  56. return self._absolute_start
  57. @property
  58. def channels(self):
  59. """
  60. These are labels created by the NIS Elements user. Typically they may a short description of the filter cube
  61. used (e.g. "bright field", "GFP", etc.)
  62. :rtype: str
  63. """
  64. if not self._channels:
  65. self._channels = []
  66. metadata = self.metadata[six.b('ImageMetadataSeq')][six.b('SLxPictureMetadata')][six.b('sPicturePlanes')]
  67. try:
  68. validity = self.metadata[six.b('ImageMetadata')][six.b('SLxExperiment')][six.b('ppNextLevelEx')][six.b('')][0][six.b('ppNextLevelEx')][six.b('')][0][six.b('pItemValid')]
  69. except KeyError:
  70. # If none of the channels have been deleted, there is no validity list, so we just make one
  71. validity = [True for _ in metadata]
  72. # Channel information is contained in dictionaries with the keys a0, a1...an where the number
  73. # indicates the order in which the channel is stored. So by sorting the dicts alphabetically
  74. # we get the correct order.
  75. for (label, chan), valid in zip(sorted(metadata[six.b('sPlaneNew')].items()), validity):
  76. if not valid:
  77. continue
  78. self._channels.append(chan[six.b('sDescription')].decode("utf8"))
  79. return self._channels
  80. @property
  81. def fields_of_view(self):
  82. """
  83. The metadata contains information about fields of view, but it contains it even if some fields
  84. of view were cropped. We can't find anything that states which fields of view are actually
  85. in the image data, so we have to calculate it. There probably is something somewhere, since
  86. NIS Elements can figure it out, but we haven't found it yet.
  87. :rtype: int
  88. """
  89. if self._fields_of_view is None:
  90. self._fields_of_view = self._parse_dimension_text(r""".*?XY\((\d+)\).*?""")
  91. return self._fields_of_view
  92. @property
  93. def time_indexes(self):
  94. """
  95. The number of cycles.
  96. :rtype: int
  97. """
  98. if self._time_indexes is None:
  99. self._time_indexes = self._parse_dimension_text(r""".*?T'\((\d+)\).*?""")
  100. return self._time_indexes
  101. @property
  102. def z_levels(self):
  103. """
  104. The different levels in the Z-plane. Just a sequence from 0 to n.
  105. :rtype: int
  106. """
  107. if self._z_levels is None:
  108. self._z_levels = self._parse_dimension_text(r""".*?Z\((\d+)\).*?""")
  109. return self._z_levels
  110. def _calculate_field_of_view(self, frame_number):
  111. images_per_cycle = len(self.z_levels) * len(self.channels)
  112. return int((frame_number - (frame_number % images_per_cycle)) / images_per_cycle) % len(self.fields_of_view)
  113. def _calculate_channel(self, frame_number):
  114. return self._channels[frame_number % len(self.channels)]
  115. def _calculate_z_level(self, frame_number):
  116. return self.z_levels[int(((frame_number - (frame_number % len(self.channels))) / len(self.channels)) % len(self.z_levels))]
  117. @property
  118. def _file_handle(self):
  119. if self._fh is None:
  120. self._fh = open(self._filename, "rb")
  121. return self._fh
  122. def _get_raw_image_data(self, image_group_number, channel_offset):
  123. """
  124. Reads the raw bytes and the timestamp of an image.
  125. :param image_group_number: groups are made of images with the same time index, field of view and z-level.
  126. :type image_group_number: int
  127. :param channel_offset: the offset in the array where the bytes for this image are found.
  128. :type channel_offset: int
  129. :return: (int, array.array()) or None
  130. """
  131. chunk = self._label_map[six.b("ImageDataSeq|%d!" % image_group_number)]
  132. data = self._read_chunk(chunk)
  133. # All images in the same image group share the same timestamp! So if you have complicated image data,
  134. # your timestamps may not be entirely accurate. Practically speaking though, they'll only be off by a few
  135. # seconds unless you're doing something super weird.
  136. timestamp = struct.unpack("d", data[:8])[0]
  137. image_group_data = array.array("H", data)
  138. image_data_start = 4 + channel_offset
  139. # The images for the various channels are interleaved within the same array. For example, the second image
  140. # of a four image group will be composed of bytes 2, 6, 10, etc. If you understand why someone would design
  141. # a data structure that way, please send the author of this library a message.
  142. image_data = image_group_data[image_data_start::len(self.channels)]
  143. # Skip images that are all zeros! This is important, since NIS Elements creates blank "gap" images if you
  144. # don't have the same number of images each cycle. We discovered this because we only took GFP images every
  145. # other cycle to reduce phototoxicity, but NIS Elements still allocated memory as if we were going to take
  146. # them every cycle.
  147. if np.any(image_data):
  148. return timestamp, image_data
  149. return None
  150. @property
  151. def _dimensions(self):
  152. """
  153. While there are metadata values that represent a lot of what we want to capture, they seem to be unreliable.
  154. Sometimes certain elements don't exist, or change their data type randomly. However, the human-readable text
  155. is always there and in the same exact format, so we just parse that instead.
  156. :rtype: str
  157. """
  158. if self._dimension_text is None:
  159. for line in self.metadata[six.b('ImageTextInfo')][six.b('SLxImageTextInfo')].values():
  160. if six.b("Dimensions:") in line:
  161. metadata = line
  162. break
  163. else:
  164. raise ValueError("Could not parse metadata dimensions!")
  165. for line in metadata.split(six.b("\r\n")):
  166. if line.startswith(six.b("Dimensions:")):
  167. self._dimension_text = line
  168. break
  169. else:
  170. raise ValueError("Could not parse metadata dimensions!")
  171. return self._dimension_text
  172. def _calculate_image_group_number(self, time_index, fov, z_level):
  173. """
  174. Images are grouped together if they share the same time index, field of view, and z-level.
  175. :type time_index: int
  176. :type fov: int
  177. :type z_level: int
  178. :rtype: int
  179. """
  180. return time_index * len(self.fields_of_view) * len(self.z_levels) + (fov * len(self.z_levels) + z_level)
  181. @property
  182. def _channel_offset(self):
  183. """
  184. Image data is interleaved for each image set. That is, if there are four images in a set, the first image
  185. will consist of pixels 1, 5, 9, etc, the second will be pixels 2, 6, 10, and so forth.
  186. :rtype: dict
  187. """
  188. channel_offset = {}
  189. for n, channel in enumerate(self._channels):
  190. channel_offset[channel] = n
  191. return channel_offset
  192. def _parse_dimension_text(self, pattern):
  193. try:
  194. count = int(re.match(pattern, self._dimensions).group(1))
  195. except AttributeError:
  196. return [0]
  197. except TypeError:
  198. count = int(re.match(pattern, self._dimensions.decode("utf8")).group(1))
  199. return list(range(count))
  200. else:
  201. return list(range(count))
  202. @property
  203. def _total_images_per_channel(self):
  204. """
  205. The total number of images per channel. Warning: this may be inaccurate as it includes "gap" images.
  206. :rtype: int
  207. """
  208. return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiSequenceCount')]
  209. def _parse_metadata(self):
  210. """
  211. Reads all metadata.
  212. """
  213. for label in self._label_map.keys():
  214. if label.endswith(six.b("LV!")) or six.b("LV|") in label:
  215. data = self._read_chunk(self._label_map[label])
  216. stop = label.index(six.b("LV"))
  217. self.metadata[label[:stop]] = self._read_metadata(data, 1)
  218. def _read_map(self):
  219. """
  220. Every label ends with an exclamation point, however, we can't directly search for those to find all the labels
  221. as some of the bytes contain the value 33, which is the ASCII code for "!". So we iteratively find each label,
  222. grab the subsequent data (always 16 bytes long), advance to the next label and repeat.
  223. """
  224. self._file_handle.seek(-8, 2)
  225. chunk_map_start_location = struct.unpack("Q", self._file_handle.read(8))[0]
  226. self._file_handle.seek(chunk_map_start_location)
  227. raw_text = self._file_handle.read(-1)
  228. label_start = raw_text.index(Nd2Parser.CHUNK_MAP_START) + 32
  229. while True:
  230. data_start = raw_text.index(six.b("!"), label_start) + 1
  231. key = raw_text[label_start: data_start]
  232. location, length = struct.unpack("QQ", raw_text[data_start: data_start + 16])
  233. if key == Nd2Parser.CHUNK_MAP_END:
  234. # We've reached the end of the chunk map
  235. break
  236. self._label_map[key] = location
  237. label_start = data_start + 16
  238. def _read_chunk(self, chunk_location):
  239. """
  240. Gets the data for a given chunk pointer
  241. """
  242. self._file_handle.seek(chunk_location)
  243. # The chunk metadata is always 16 bytes long
  244. chunk_metadata = self._file_handle.read(16)
  245. header, relative_offset, data_length = struct.unpack("IIQ", chunk_metadata)
  246. if header != Nd2Parser.CHUNK_HEADER:
  247. raise ValueError("The ND2 file seems to be corrupted.")
  248. # We start at the location of the chunk metadata, skip over the metadata, and then proceed to the
  249. # start of the actual data field, which is at some arbitrary place after the metadata.
  250. self._file_handle.seek(chunk_location + 16 + relative_offset)
  251. return self._file_handle.read(data_length)
  252. def _parse_unsigned_char(self, data):
  253. return struct.unpack("B", data.read(1))[0]
  254. def _parse_unsigned_int(self, data):
  255. return struct.unpack("I", data.read(4))[0]
  256. def _parse_unsigned_long(self, data):
  257. return struct.unpack("Q", data.read(8))[0]
  258. def _parse_double(self, data):
  259. return struct.unpack("d", data.read(8))[0]
  260. def _parse_string(self, data):
  261. value = data.read(2)
  262. while not value.endswith(six.b("\x00\x00")):
  263. # the string ends at the first instance of \x00\x00
  264. value += data.read(2)
  265. return value.decode("utf16")[:-1].encode("utf8")
  266. def _parse_char_array(self, data):
  267. array_length = struct.unpack("Q", data.read(8))[0]
  268. return array.array("B", data.read(array_length))
  269. def _parse_metadata_item(self, data):
  270. """
  271. Reads hierarchical data, analogous to a Python dict.
  272. """
  273. new_count, length = struct.unpack("<IQ", data.read(12))
  274. length -= data.tell() - self._cursor_position
  275. next_data_length = data.read(length)
  276. value = self._read_metadata(next_data_length, new_count)
  277. # Skip some offsets
  278. data.read(new_count * 8)
  279. return value
  280. def _get_value(self, data, data_type):
  281. """
  282. ND2s use various codes to indicate different data types, which we translate here.
  283. """
  284. parser = {1: self._parse_unsigned_char,
  285. 2: self._parse_unsigned_int,
  286. 3: self._parse_unsigned_int,
  287. 5: self._parse_unsigned_long,
  288. 6: self._parse_double,
  289. 8: self._parse_string,
  290. 9: self._parse_char_array,
  291. 11: self._parse_metadata_item}
  292. return parser[data_type](data)
  293. def _read_metadata(self, data, count):
  294. """
  295. Iterates over each element some section of the metadata and parses it.
  296. """
  297. data = six.BytesIO(data)
  298. metadata = {}
  299. for _ in range(count):
  300. self._cursor_position = data.tell()
  301. header = data.read(2)
  302. if not header:
  303. # We've reached the end of some hierarchy of data
  304. break
  305. if six.PY3:
  306. header = header.decode("utf8")
  307. data_type, name_length = map(ord, header)
  308. name = data.read(name_length * 2).decode("utf16")[:-1].encode("utf8")
  309. value = self._get_value(data, data_type)
  310. if name not in metadata.keys():
  311. metadata[name] = value
  312. else:
  313. if not isinstance(metadata[name], list):
  314. # We have encountered this key exactly once before. Since we're seeing it again, we know we
  315. # need to convert it to a list before proceeding.
  316. metadata[name] = [metadata[name]]
  317. # We've encountered this key before so we're guaranteed to be dealing with a list. Thus we append
  318. # the value to the already-existing list.
  319. metadata[name].append(value)
  320. return metadata