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.

176 lines
6.8 KiB

  1. # -*- coding: utf-8 -*-
  2. import array
  3. from collections import namedtuple
  4. import struct
  5. from StringIO import StringIO
  6. field_of_view = namedtuple('FOV', ['number', 'x', 'y', 'z', 'pfs_offset'])
  7. class Nd2Parser(object):
  8. """
  9. Reads .nd2 files, provides an interface to the metadata, and generates numpy arrays from the image data.
  10. """
  11. CHUNK_HEADER = 0xabeceda
  12. CHUNK_MAP_START = "ND2 FILEMAP SIGNATURE NAME 0001!"
  13. CHUNK_MAP_END = "ND2 CHUNK MAP SIGNATURE 0000001!"
  14. def __init__(self, filename):
  15. self._absolute_start = None
  16. self._filename = filename
  17. self._fh = None
  18. self._chunk_map_start_location = None
  19. self._cursor_position = 0
  20. self._dimension_text = None
  21. self._label_map = {}
  22. self.metadata = {}
  23. self._read_map()
  24. self._parse_metadata()
  25. @property
  26. def _file_handle(self):
  27. if self._fh is None:
  28. self._fh = open(self._filename, "rb")
  29. return self._fh
  30. @property
  31. def _dimensions(self):
  32. if self._dimension_text is None:
  33. for line in self.metadata['ImageTextInfo']['SLxImageTextInfo'].values():
  34. if "Dimensions:" in line:
  35. metadata = line
  36. break
  37. else:
  38. raise ValueError("Could not parse metadata dimensions!")
  39. for line in metadata.split("\r\n"):
  40. if line.startswith("Dimensions:"):
  41. self._dimension_text = line
  42. break
  43. else:
  44. raise ValueError("Could not parse metadata dimensions!")
  45. return self._dimension_text
  46. @property
  47. def _image_count(self):
  48. return self.metadata['ImageAttributes']['SLxImageAttributes']['uiSequenceCount']
  49. @property
  50. def _sequence_count(self):
  51. return self.metadata['ImageEvents']['RLxExperimentRecord']['uiCount']
  52. def _parse_metadata(self):
  53. for label in self._label_map.keys():
  54. if label.endswith("LV!") or "LV|" in label:
  55. data = self._read_chunk(self._label_map[label])
  56. stop = label.index("LV")
  57. self.metadata[label[:stop]] = self._read_metadata(data, 1)
  58. def _read_map(self):
  59. """
  60. Every label ends with an exclamation point, however, we can't directly search for those to find all the labels
  61. as some of the bytes contain the value 33, which is the ASCII code for "!". So we iteratively find each label,
  62. grab the subsequent data (always 16 bytes long), advance to the next label and repeat.
  63. """
  64. self._file_handle.seek(-8, 2)
  65. chunk_map_start_location = struct.unpack("Q", self._file_handle.read(8))[0]
  66. self._file_handle.seek(chunk_map_start_location)
  67. raw_text = self._file_handle.read(-1)
  68. label_start = raw_text.index(Nd2Parser.CHUNK_MAP_START) + 32
  69. while True:
  70. data_start = raw_text.index("!", label_start) + 1
  71. key = raw_text[label_start: data_start]
  72. location, length = struct.unpack("QQ", raw_text[data_start: data_start + 16])
  73. if key == Nd2Parser.CHUNK_MAP_END:
  74. # We've reached the end of the chunk map
  75. break
  76. self._label_map[key] = location
  77. label_start = data_start + 16
  78. def _read_chunk(self, chunk_location):
  79. """
  80. Gets the data for a given chunk pointer
  81. """
  82. self._file_handle.seek(chunk_location)
  83. # The chunk metadata is always 16 bytes long
  84. chunk_metadata = self._file_handle.read(16)
  85. header, relative_offset, data_length = struct.unpack("IIQ", chunk_metadata)
  86. if header != Nd2Parser.CHUNK_HEADER:
  87. raise ValueError("The ND2 file seems to be corrupted.")
  88. # We start at the location of the chunk metadata, skip over the metadata, and then proceed to the
  89. # start of the actual data field, which is at some arbitrary place after the metadata.
  90. self._file_handle.seek(chunk_location + 16 + relative_offset)
  91. return self._file_handle.read(data_length)
  92. def _z_level_count(self):
  93. st = self._read_chunk(self._label_map["CustomData|Z!"])
  94. return len(array.array("d", st))
  95. def _parse_unsigned_char(self, data):
  96. return struct.unpack("B", data.read(1))[0]
  97. def _parse_unsigned_int(self, data):
  98. return struct.unpack("I", data.read(4))[0]
  99. def _parse_unsigned_long(self, data):
  100. return struct.unpack("Q", data.read(8))[0]
  101. def _parse_double(self, data):
  102. return struct.unpack("d", data.read(8))[0]
  103. def _parse_string(self, data):
  104. value = data.read(2)
  105. while not value.endswith("\x00\x00"):
  106. # the string ends at the first instance of \x00\x00
  107. value += data.read(2)
  108. return value.decode("utf16")[:-1].encode("utf8")
  109. def _parse_char_array(self, data):
  110. array_length = struct.unpack("Q", data.read(8))[0]
  111. return array.array("B", data.read(array_length))
  112. def _parse_metadata_item(self, data):
  113. new_count, length = struct.unpack("<IQ", data.read(12))
  114. length -= data.tell() - self._cursor_position
  115. next_data_length = data.read(length)
  116. value = self._read_metadata(next_data_length, new_count)
  117. # Skip some offsets
  118. data.read(new_count * 8)
  119. return value
  120. def _get_value(self, data, data_type):
  121. parser = {1: self._parse_unsigned_char,
  122. 2: self._parse_unsigned_int,
  123. 3: self._parse_unsigned_int,
  124. 5: self._parse_unsigned_long,
  125. 6: self._parse_double,
  126. 8: self._parse_string,
  127. 9: self._parse_char_array,
  128. 11: self._parse_metadata_item}
  129. return parser[data_type](data)
  130. def _read_metadata(self, data, count):
  131. data = StringIO(data)
  132. metadata = {}
  133. for _ in xrange(count):
  134. self._cursor_position = data.tell()
  135. header = data.read(2)
  136. if not header:
  137. break
  138. data_type, name_length = map(ord, header)
  139. name = data.read(name_length * 2).decode("utf16")[:-1].encode("utf8")
  140. value = self._get_value(data, data_type)
  141. if name not in metadata.keys():
  142. metadata[name] = value
  143. else:
  144. if not isinstance(metadata[name], list):
  145. # We have encountered this key exactly once before. Since we're seeing it again, we know we
  146. # need to convert it to a list before proceeding.
  147. metadata[name] = [metadata[name]]
  148. # We've encountered this key before so we're guaranteed to be dealing with a list. Thus we append
  149. # the value to the already-existing list.
  150. metadata[name].append(value)
  151. return metadata