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.

133 lines
4.8 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. class Nd2Reader(object):
  5. """
  6. Reads .nd2 files, provides an interface to the metadata, and generates numpy arrays from the image data.
  7. """
  8. def __init__(self, filename):
  9. self._filename = filename
  10. self._file_handler = None
  11. self._chunk_map_start_location = None
  12. self._label_map = {}
  13. self._read_map()
  14. @property
  15. def fh(self):
  16. if self._file_handler is None:
  17. self._file_handler = open(self._filename, "rb")
  18. return self._file_handler
  19. def _read_map(self):
  20. """
  21. Every label ends with an exclamation point, however, we can't directly search for those to find all the labels
  22. as some of the bytes contain the value 33, which is the ASCII code for "!". So we iteratively find each label,
  23. grab the subsequent data (always 16 bytes long), advance to the next label and repeat.
  24. """
  25. raw_text = self._get_raw_chunk_map_text()
  26. label_start = self._find_first_label_offset(raw_text)
  27. while True:
  28. data_start = self._get_data_start(label_start, raw_text)
  29. label, value = self._extract_map_key(label_start, data_start, raw_text)
  30. if label == "ND2 CHUNK MAP SIGNATURE 0000001!":
  31. # We've reached the end of the chunk map
  32. break
  33. self._label_map[label] = value
  34. label_start = data_start + 16
  35. @staticmethod
  36. def _find_first_label_offset(raw_text):
  37. """
  38. The chunk map starts with some number of (seemingly) useless bytes, followed
  39. by "ND2 FILEMAP SIGNATURE NAME 0001!". We return the location of the first character after this sequence,
  40. which is the actual beginning of the chunk map.
  41. """
  42. return raw_text.index("ND2 FILEMAP SIGNATURE NAME 0001!") + 32
  43. @staticmethod
  44. def _get_data_start(label_start, raw_text):
  45. """
  46. The data for a given label begins immediately after the first exclamation point
  47. """
  48. return raw_text.index("!", label_start) + 1
  49. @staticmethod
  50. def _extract_map_key(label_start, data_start, raw_text):
  51. """
  52. Chunk map entries are a string label of arbitrary length followed by 16 bytes of data, which represent
  53. the byte offset from the beginning of the file where that data can be found.
  54. """
  55. key = raw_text[label_start: data_start]
  56. value = struct.unpack("QQ", raw_text[data_start: data_start + 16])
  57. return key, value
  58. @property
  59. def chunk_map_start_location(self):
  60. """
  61. The position in bytes from the beginning of the file where the chunk map begins.
  62. The chunk map is a series of string labels followed by the position (in bytes) of the respective data.
  63. """
  64. if self._chunk_map_start_location is None:
  65. # Put the cursor 8 bytes before the end of the file
  66. self.fh.seek(-8, 2)
  67. # Read the last 8 bytes of the file
  68. self._chunk_map_start_location = struct.unpack("Q", self.fh.read(8))[0]
  69. return self._chunk_map_start_location
  70. def _read_chunk(self, chunk_location):
  71. """
  72. Gets the data for a given chunk pointer
  73. """
  74. self.fh.seek(chunk_location)
  75. chunk_data = self._read_chunk_metadata()
  76. header, relative_offset, data_length = self._parse_chunk_metadata(chunk_data)
  77. return self._read_chunk_data(chunk_location, relative_offset, data_length)
  78. def _read_chunk_metadata(self):
  79. """
  80. Gets the chunks metadata, which is always 16 bytes
  81. """
  82. return self.fh.read(16)
  83. def _read_chunk_data(self, chunk_location, relative_offset, data_length):
  84. """
  85. Reads the actual data for a given chunk
  86. """
  87. # We start at the location of the chunk metadata, skip over the metadata, and then proceed to the
  88. # start of the actual data field, which is at some arbitrary place after the metadata.
  89. self.fh.seek(chunk_location + 16 + relative_offset)
  90. return self.fh.read(data_length)
  91. @staticmethod
  92. def _parse_chunk_metadata(chunk_data):
  93. """
  94. Finds out everything about a given chunk. Every chunk begins with the same value, so if that's ever
  95. different we can assume the file has suffered some kind of damage.
  96. """
  97. header, relative_offset, data_length = struct.unpack("IIQ", chunk_data)
  98. if header != 0xabeceda:
  99. raise ValueError("The ND2 file seems to be corrupted.")
  100. return header, relative_offset, data_length
  101. def _get_raw_chunk_map_text(self):
  102. """
  103. Reads the entire chunk map and returns it as a string.
  104. """
  105. self.fh.seek(self.chunk_map_start_location)
  106. return self.fh.read(-1)
  107. @staticmethod
  108. def as_numpy_array(arr):
  109. return np.frombuffer(arr)