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.

323 lines
12 KiB

10 years ago
10 years ago
10 years ago
10 years ago
  1. import array
  2. import numpy as np
  3. import struct
  4. from collections import namedtuple
  5. from StringIO import StringIO
  6. from nd2reader.model import Channel
  7. from pprint import pprint
  8. chunk = namedtuple('Chunk', ['location', 'length'])
  9. field_of_view = namedtuple('FOV', ['number', 'x', 'y', 'z', 'pfs_offset'])
  10. class Nd2(object):
  11. def __init__(self, filename):
  12. self._parser = Nd2Parser(filename)
  13. @property
  14. def timepoint_count(self):
  15. return len(self._parser.metadata['ImageEvents']['RLxExperimentRecord']['pEvents'][''])
  16. @property
  17. def height(self):
  18. return self._parser.metadata['ImageAttributes']['SLxImageAttributes']['uiHeight']
  19. @property
  20. def width(self):
  21. return self._parser.metadata['ImageAttributes']['SLxImageAttributes']['uiWidth']
  22. @property
  23. def fields_of_view(self):
  24. fov_data = self.metadata['ImageMetadata']['SLxExperiment']['ppNextLevelEx']['']
  25. valid_fields = list(fov_data['pItemValid'])
  26. for number, (fov, valid) in enumerate(zip(fov_data['uLoopPars']['Points'][''], valid_fields)):
  27. if valid:
  28. yield field_of_view(number=number + 1,
  29. x=fov['dPosX'],
  30. y=fov['dPosY'],
  31. z=fov['dPosZ'],
  32. pfs_offset=fov['dPFSOffset'])
  33. @property
  34. def fov_count(self):
  35. """
  36. The metadata contains information about fields of view, but it contains it even if some fields
  37. of view were cropped. We can't find anything that states which fields of view are actually
  38. in the image data, so we have to calculate it. There probably is something somewhere, since
  39. NIS Elements can figure it out, but we haven't found it yet.
  40. """
  41. return sum(self.metadata['ImageMetadata']['SLxExperiment']['ppNextLevelEx']['']['pItemValid'])
  42. @property
  43. def channels(self):
  44. metadata = self.metadata['ImageMetadataSeq']['SLxPictureMetadata']['sPicturePlanes']
  45. # Channel information is contained in dictionaries with the keys a0, a1...an where the number
  46. # indicates the order in which the channel is stored. So by sorting the dicts alphabetically
  47. # we get the correct order.
  48. for label, chan in sorted(metadata['sPlaneNew'].items()):
  49. name = chan['sDescription']
  50. exposure_time = metadata['sSampleSetting'][label]['dExposureTime']
  51. camera = metadata['sSampleSetting'][label]['pCameraSetting']['CameraUserName']
  52. yield Channel(name, camera, exposure_time)
  53. @property
  54. def channel_count(self):
  55. return self.metadata['ImageAttributes']["SLxImageAttributes"]["uiComp"]
  56. @property
  57. def z_level_count(self):
  58. """
  59. The number of different z-axis levels.
  60. """
  61. return 1
  62. @property
  63. def metadata(self):
  64. return self._parser.metadata
  65. def get_images(self, fov_number, channel_name, z_axis):
  66. pass
  67. def get_image(self, nr):
  68. d = self._parser._read_chunk(self._parser._label_map["ImageDataSeq|%d!" % nr].location)
  69. timestamp = struct.unpack("d", d[:8])[0]
  70. res = [timestamp]
  71. # The images for the various channels are interleaved within each other.
  72. for i in range(self.channel_count):
  73. a = array.array("H", d)
  74. res.append(a[4+i::self.channel_count])
  75. # TODO: Are you missing a zoom level? Is there extra data here? Can you get timestamps now?
  76. return res
  77. class Nd2Parser(object):
  78. """
  79. Reads .nd2 files, provides an interface to the metadata, and generates numpy arrays from the image data.
  80. """
  81. def __init__(self, filename):
  82. self._filename = filename
  83. self._file_handler = None
  84. self._chunk_map_start_location = None
  85. self._label_map = {}
  86. self._metadata = {}
  87. self._read_map()
  88. self._parse_dict_data()
  89. @property
  90. def fh(self):
  91. if self._file_handler is None:
  92. self._file_handler = open(self._filename, "rb")
  93. return self._file_handler
  94. def _parse_dict_data(self):
  95. # TODO: Don't like this name
  96. for label in self._top_level_dict_labels:
  97. chunk_location = self._label_map[label].location
  98. data = self._read_chunk(chunk_location)
  99. stop = label.index("LV")
  100. self._metadata[label[:stop]] = self.read_lv_encoding(data, 1)
  101. @property
  102. def metadata(self):
  103. return self._metadata
  104. @property
  105. def _top_level_dict_labels(self):
  106. # TODO: I don't like this name either
  107. for label in self._label_map.keys():
  108. if label.endswith("LV!") or "LV|" in label:
  109. yield label
  110. def _read_map(self):
  111. """
  112. Every label ends with an exclamation point, however, we can't directly search for those to find all the labels
  113. as some of the bytes contain the value 33, which is the ASCII code for "!". So we iteratively find each label,
  114. grab the subsequent data (always 16 bytes long), advance to the next label and repeat.
  115. """
  116. raw_text = self._get_raw_chunk_map_text()
  117. label_start = self._find_first_label_offset(raw_text)
  118. while True:
  119. data_start = self._get_data_start(label_start, raw_text)
  120. label, value = self._extract_map_key(label_start, data_start, raw_text)
  121. if label == "ND2 CHUNK MAP SIGNATURE 0000001!":
  122. # We've reached the end of the chunk map
  123. break
  124. self._label_map[label] = value
  125. label_start = data_start + 16
  126. @staticmethod
  127. def _find_first_label_offset(raw_text):
  128. """
  129. The chunk map starts with some number of (seemingly) useless bytes, followed
  130. by "ND2 FILEMAP SIGNATURE NAME 0001!". We return the location of the first character after this sequence,
  131. which is the actual beginning of the chunk map.
  132. """
  133. return raw_text.index("ND2 FILEMAP SIGNATURE NAME 0001!") + 32
  134. @staticmethod
  135. def _get_data_start(label_start, raw_text):
  136. """
  137. The data for a given label begins immediately after the first exclamation point
  138. """
  139. return raw_text.index("!", label_start) + 1
  140. @staticmethod
  141. def _extract_map_key(label_start, data_start, raw_text):
  142. """
  143. Chunk map entries are a string label of arbitrary length followed by 16 bytes of data, which represent
  144. the byte offset from the beginning of the file where that data can be found.
  145. """
  146. key = raw_text[label_start: data_start]
  147. location, length = struct.unpack("QQ", raw_text[data_start: data_start + 16])
  148. return key, chunk(location=location, length=length)
  149. @property
  150. def chunk_map_start_location(self):
  151. """
  152. The position in bytes from the beginning of the file where the chunk map begins.
  153. The chunk map is a series of string labels followed by the position (in bytes) of the respective data.
  154. """
  155. if self._chunk_map_start_location is None:
  156. # Put the cursor 8 bytes before the end of the file
  157. self.fh.seek(-8, 2)
  158. # Read the last 8 bytes of the file
  159. self._chunk_map_start_location = struct.unpack("Q", self.fh.read(8))[0]
  160. return self._chunk_map_start_location
  161. def _read_chunk(self, chunk_location):
  162. """
  163. Gets the data for a given chunk pointer
  164. """
  165. self.fh.seek(chunk_location)
  166. chunk_data = self._read_chunk_metadata()
  167. header, relative_offset, data_length = self._parse_chunk_metadata(chunk_data)
  168. return self._read_chunk_data(chunk_location, relative_offset, data_length)
  169. def _read_chunk_metadata(self):
  170. """
  171. Gets the chunks metadata, which is always 16 bytes
  172. """
  173. return self.fh.read(16)
  174. def _read_chunk_data(self, chunk_location, relative_offset, data_length):
  175. """
  176. Reads the actual data for a given chunk
  177. """
  178. # We start at the location of the chunk metadata, skip over the metadata, and then proceed to the
  179. # start of the actual data field, which is at some arbitrary place after the metadata.
  180. self.fh.seek(chunk_location + 16 + relative_offset)
  181. return self.fh.read(data_length)
  182. @staticmethod
  183. def _parse_chunk_metadata(chunk_data):
  184. """
  185. Finds out everything about a given chunk. Every chunk begins with the same value, so if that's ever
  186. different we can assume the file has suffered some kind of damage.
  187. """
  188. header, relative_offset, data_length = struct.unpack("IIQ", chunk_data)
  189. if header != 0xabeceda:
  190. raise ValueError("The ND2 file seems to be corrupted.")
  191. return header, relative_offset, data_length
  192. def _get_raw_chunk_map_text(self):
  193. """
  194. Reads the entire chunk map and returns it as a string.
  195. """
  196. self.fh.seek(self.chunk_map_start_location)
  197. return self.fh.read(-1)
  198. @staticmethod
  199. def as_numpy_array(arr):
  200. return np.frombuffer(arr)
  201. def read_lv_encoding(self, data, count):
  202. data = StringIO(data)
  203. res = {}
  204. for c in range(count):
  205. lastpos = data.tell()
  206. hdr = data.read(2)
  207. if not hdr:
  208. break
  209. typ = ord(hdr[0])
  210. bname = data.read(2*ord(hdr[1]))
  211. name = bname.decode("utf16")[:-1].encode("utf8")
  212. if typ == 1:
  213. value, = struct.unpack("B", data.read(1))
  214. elif typ in [2, 3]:
  215. value, = struct.unpack("I", data.read(4))
  216. elif typ == 5:
  217. value, = struct.unpack("Q", data.read(8))
  218. elif typ == 6:
  219. value, = struct.unpack("d", data.read(8))
  220. elif typ == 8:
  221. value = data.read(2)
  222. while value[-2:] != "\x00\x00":
  223. value += data.read(2)
  224. value = value.decode("utf16")[:-1].encode("utf8")
  225. elif typ == 9:
  226. cnt, = struct.unpack("Q", data.read(8))
  227. value = array.array("B", data.read(cnt))
  228. elif typ == 11:
  229. newcount, length = struct.unpack("<IQ", data.read(12))
  230. length -= data.tell()-lastpos
  231. nextdata = data.read(length)
  232. value = self.read_lv_encoding(nextdata, newcount)
  233. # XXX do not know for what these offsets? are
  234. unknown = array.array("I", data.read(newcount*8))
  235. else:
  236. assert 0, "%s hdr %x:%x unknown" % (name, ord(hdr[0]), ord(hdr[1]))
  237. if not name in res:
  238. res[name] = value
  239. else:
  240. if not isinstance(res[name], list):
  241. res[name] = [res[name]]
  242. res[name].append(value)
  243. x = data.read()
  244. assert not x, "skip %d %s" % (len(x), repr(x[:30]))
  245. return res
  246. #
  247. # class LVLine(object):
  248. # def __init__(self, line):
  249. # self._line = line
  250. # self._extract()
  251. #
  252. # def _extract(self):
  253. # if self._type == 11:
  254. # count, length = struct.unpack("<IQ", self._line[self._name_end: self._name_end + 12])
  255. # newline = self._line[self._name_end + 12:]
  256. #
  257. # @property
  258. # def name(self):
  259. # return self._line[2: self._name_end].decode("utf16").encode("utf8")
  260. #
  261. # @property
  262. # def _type(self):
  263. # return ord(self._line[0])
  264. #
  265. # @property
  266. # def _name_end(self):
  267. # """
  268. # Length is given as number of characters, but since it's unicode (which is two-bytes per character) we return
  269. # twice the number.
  270. #
  271. # """
  272. # return ord(self._line[1]) * 2
  273. #
  274. #
  275. # class LVData(object):
  276. # def __init__(self, data):
  277. # self._extracted_data = LVLine(data)