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.

410 lines
16 KiB

4 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
6 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
4 years ago
7 years ago
4 years ago
7 years ago
7 years ago
4 years ago
6 years ago
4 years ago
4 years ago
8 years ago
7 years ago
4 years ago
  1. # -*- coding: utf-8 -*-
  2. import struct
  3. import array
  4. import six
  5. import warnings
  6. from pims.base_frames import Frame
  7. import numpy as np
  8. from tqdm import tqdm
  9. from nd2reader.common import get_version, read_chunk
  10. from nd2reader.label_map import LabelMap
  11. from nd2reader.raw_metadata import RawMetadata
  12. from nd2reader import stitched
  13. class Parser(object):
  14. """Parses ND2 files and creates a Metadata and driver object.
  15. """
  16. CHUNK_HEADER = 0xabeceda
  17. CHUNK_MAP_START = six.b("ND2 FILEMAP SIGNATURE NAME 0001!")
  18. CHUNK_MAP_END = six.b("ND2 CHUNK MAP SIGNATURE 0000001!")
  19. supported_file_versions = {(3, None): True}
  20. def __init__(self, fh):
  21. self._fh = fh
  22. self._label_map = None
  23. self._raw_metadata = None
  24. self.metadata = None
  25. # First check the file version
  26. self.supported = self._check_version_supported()
  27. # Parse the metadata
  28. self._parse_metadata()
  29. def calculate_image_properties(self, index):
  30. """Calculate FOV, channels and z_levels
  31. Args:
  32. index(int): the index (which is simply the order in which the image was acquired)
  33. Returns:
  34. tuple: tuple of the field of view, the channel and the z level
  35. """
  36. field_of_view = self._calculate_field_of_view(index)
  37. channel = self._calculate_channel(index)
  38. z_level = self._calculate_z_level(index)
  39. return field_of_view, channel, z_level
  40. def get_image(self, index):
  41. """
  42. Creates an Image object and adds its metadata, based on the index (which is simply the order in which the image
  43. was acquired). May return None if the ND2 contains multiple channels and not all were taken in each cycle (for
  44. example, if you take bright field images every minute, and GFP images every five minutes, there will be some
  45. indexes that do not contain an image. The reason for this is complicated, but suffice it to say that we hope to
  46. eliminate this possibility in future releases. For now, you'll need to check if your image is None if you're
  47. doing anything out of the ordinary.
  48. Args:
  49. index(int): the index (which is simply the order in which the image was acquired)
  50. Returns:
  51. Frame: the image
  52. """
  53. field_of_view, channel, z_level = self.calculate_image_properties(index)
  54. channel_offset = index % len(self.metadata["channels"])
  55. image_group_number = int(index / len(self.metadata["channels"]))
  56. frame_number = self._calculate_frame_number(image_group_number, field_of_view, z_level)
  57. try:
  58. timestamp, image = self._get_raw_image_data(image_group_number, channel_offset, self.metadata["height"],
  59. self.metadata["width"])
  60. except (TypeError):
  61. return Frame([], frame_no=frame_number, metadata=self._get_frame_metadata())
  62. else:
  63. return Frame(image, frame_no=frame_number, metadata=self._get_frame_metadata())
  64. def get_slice_by_attributes(self, xywh, frame_number, field_of_view, channel, z_level, height, width):
  65. """Gets a rectangular slice of an image based on its attributes alone
  66. Args:
  67. xywh: tuples containing (x, y, w, h) values of the
  68. rectangular region to load
  69. frame_number: the frame number
  70. field_of_view: the field of view
  71. channel_name: the color channel name
  72. z_level: the z level
  73. height: the height of the image
  74. width: the width of the image
  75. Returns:
  76. Frame: the requested image
  77. """
  78. frame_number = 0 if frame_number is None else frame_number
  79. field_of_view = 0 if field_of_view is None else field_of_view
  80. channel = 0 if channel is None else channel
  81. z_level = 0 if z_level is None else z_level
  82. image_group_number = self._calculate_image_group_number(frame_number, field_of_view, z_level)
  83. try:
  84. timestamp, raw_image_data = self._get_raw_slice_data(
  85. xywh, image_group_number, channel, height, width
  86. )
  87. except (TypeError):
  88. return Frame([], frame_no=frame_number, metadata=self._get_frame_metadata())
  89. else:
  90. return Frame(raw_image_data, frame_no=frame_number, metadata=self._get_frame_metadata())
  91. def get_image_by_attributes(self, frame_number, field_of_view, channel, z_level, height, width):
  92. """Gets an image based on its attributes alone
  93. Args:
  94. frame_number: the frame number
  95. field_of_view: the field of view
  96. channel_name: the color channel name
  97. z_level: the z level
  98. height: the height of the image
  99. width: the width of the image
  100. Returns:
  101. Frame: the requested image
  102. """
  103. frame_number = 0 if frame_number is None else frame_number
  104. field_of_view = 0 if field_of_view is None else field_of_view
  105. channel = 0 if channel is None else channel
  106. z_level = 0 if z_level is None else z_level
  107. image_group_number = self._calculate_image_group_number(frame_number, field_of_view, z_level)
  108. try:
  109. timestamp, raw_image_data = self._get_raw_image_data(image_group_number, channel,
  110. height, width)
  111. except (TypeError):
  112. return Frame([], frame_no=frame_number, metadata=self._get_frame_metadata())
  113. else:
  114. return Frame(raw_image_data, frame_no=frame_number, metadata=self._get_frame_metadata())
  115. @staticmethod
  116. def get_dtype_from_metadata():
  117. """Determine the data type from the metadata.
  118. For now, always use float64 to prevent unexpected overflow errors when manipulating the data (calculating sums/
  119. means/etc.)
  120. """
  121. return np.float64
  122. def _check_version_supported(self):
  123. """Checks if the ND2 file version is supported by this reader.
  124. Returns:
  125. bool: True on supported
  126. """
  127. major_version, minor_version = get_version(self._fh)
  128. supported = self.supported_file_versions.get(
  129. (major_version, minor_version)) or self.supported_file_versions.get((major_version, None))
  130. if not supported:
  131. print("Warning: No parser is available for your current ND2 version (%d.%d). " % (
  132. major_version, minor_version) + "This might lead to unexpected behaviour.")
  133. return supported
  134. def _parse_metadata(self):
  135. """Reads all metadata and instantiates the Metadata object.
  136. """
  137. # Retrieve raw metadata from the label mapping
  138. self._label_map = self._build_label_map()
  139. self._raw_metadata = RawMetadata(self._fh, self._label_map)
  140. self.metadata = self._raw_metadata.__dict__
  141. self.acquisition_times = self._raw_metadata.acquisition_times
  142. def _build_label_map(self):
  143. """
  144. Every label ends with an exclamation point, however, we can't directly search for those to find all the labels
  145. as some of the bytes contain the value 33, which is the ASCII code for "!". So we iteratively find each label,
  146. grab the subsequent data (always 16 bytes long), advance to the next label and repeat.
  147. Returns:
  148. LabelMap: the computed label map
  149. """
  150. # go 8 bytes back from file end
  151. self._fh.seek(-8, 2)
  152. chunk_map_start_location = struct.unpack("Q", self._fh.read(8))[0]
  153. self._fh.seek(chunk_map_start_location)
  154. raw_text = self._fh.read(-1)
  155. return LabelMap(raw_text)
  156. def _calculate_field_of_view(self, index):
  157. """Determines what field of view was being imaged for a given image.
  158. Args:
  159. index(int): the index (which is simply the order in which the image was acquired)
  160. Returns:
  161. int: the field of view
  162. """
  163. images_per_cycle = len(self.metadata["z_levels"]) * len(self.metadata["channels"])
  164. return int((index - (index % images_per_cycle)) / images_per_cycle) % len(self.metadata["fields_of_view"])
  165. def _calculate_channel(self, index):
  166. """Determines what channel a particular image is.
  167. Args:
  168. index(int): the index (which is simply the order in which the image was acquired)
  169. Returns:
  170. string: the name of the color channel
  171. """
  172. return self.metadata["channels"][index % len(self.metadata["channels"])]
  173. def _calculate_z_level(self, index):
  174. """Determines the plane in the z-axis a given image was taken in.
  175. In the future, this will be replaced with the actual offset in micrometers.
  176. Args:
  177. index(int): the index (which is simply the order in which the image was acquired)
  178. Returns:
  179. The z level
  180. """
  181. return self.metadata["z_levels"][int(
  182. ((index - (index % len(self.metadata["channels"]))) / len(self.metadata["channels"])) % len(
  183. self.metadata["z_levels"]))]
  184. def _calculate_image_group_number(self, frame_number, fov, z_level):
  185. """
  186. Images are grouped together if they share the same time index, field of view, and z-level.
  187. Args:
  188. frame_number: the time index
  189. fov: the field of view number
  190. z_level: the z level number
  191. Returns:
  192. int: the image group number
  193. """
  194. z_length = len(self.metadata['z_levels'])
  195. z_length = z_length if z_length > 0 else 1
  196. fields_of_view = len(self.metadata["fields_of_view"])
  197. fields_of_view = fields_of_view if fields_of_view > 0 else 1
  198. return frame_number * fields_of_view * z_length + (fov * z_length + z_level)
  199. def _calculate_frame_number(self, image_group_number, field_of_view, z_level):
  200. """
  201. Images are in the same frame if they share the same group number and field of view and are taken sequentially.
  202. Args:
  203. image_group_number: the image group number (see _calculate_image_group_number)
  204. field_of_view: the field of view number
  205. z_level: the z level number
  206. Returns:
  207. """
  208. return (image_group_number - (field_of_view * len(self.metadata["z_levels"]) + z_level)) / (len(self.metadata["fields_of_view"]) * len(self.metadata["z_levels"]))
  209. @property
  210. def _channel_offset(self):
  211. """
  212. Image data is interleaved for each image set. That is, if there are four images in a set, the first image
  213. will consist of pixels 1, 5, 9, etc, the second will be pixels 2, 6, 10, and so forth.
  214. Returns:
  215. dict: the channel offset for each channel
  216. """
  217. return {channel: n for n, channel in enumerate(self.metadata["channels"])}
  218. def _get_raw_slice_data(self, xywh, image_group_number, channel, height, width):
  219. """Reads the raw bytes and the timestamp of a rectangular slice
  220. of an image.
  221. Args:
  222. xywh: tuples containing (x, y, w, h) values of the
  223. rectangular region to load
  224. image_group_number: the image group number (see _calculate_image_group_number)
  225. channel: the position (int) of the channel to load
  226. height: the height of the image
  227. width: the width of the image
  228. Returns:
  229. """
  230. size_c = len(self.metadata["channels"])
  231. x0, y0, w, h = xywh
  232. chunk_location = self._label_map.get_image_data_location(image_group_number)
  233. fh = self._fh
  234. if chunk_location is None or fh is None:
  235. return None
  236. fh.seek(chunk_location)
  237. # The chunk metadata is always 16 bytes long
  238. chunk_metadata = fh.read(16)
  239. header, relative_offset, data_length = struct.unpack("IIQ", chunk_metadata)
  240. if header != 0xabeceda:
  241. raise ValueError("The ND2 file seems to be corrupted.")
  242. # We start at the location of the chunk metadata, skip over the metadata, and then proceed to the
  243. # start of the actual data field, which is at some arbitrary place after the metadata.
  244. fh.seek(chunk_location + 16 + relative_offset)
  245. # Read timestamp (8 bytes)
  246. timestamp = struct.unpack("d", fh.read(8))[0]
  247. # Stitched Images: evaluate number of bytes to strip
  248. # (with stitched images sometimes after each row we have a regular number of extra bytes)
  249. n_unwanted_bytes = (data_length-8) % (height*width)
  250. assert 0 == n_unwanted_bytes % height
  251. rowskip = n_unwanted_bytes // height
  252. # Read ROI: row-by-row
  253. image_start_pos = chunk_location + 16 + relative_offset + 8
  254. line_bytemask = np.zeros(size_c, dtype=np.bool)
  255. line_bytemask[channel] = True
  256. line_bytemask = np.tile(line_bytemask.repeat(2),w)
  257. def get_line(y):
  258. fh.seek(image_start_pos + size_c*2*((width)*y+x0) + y*rowskip)
  259. return np.frombuffer(fh.read(size_c*2*w), np.byte)[line_bytemask]
  260. data = [get_line(y) for y in range(y0, y0+h)]
  261. data = bytes().join(data)
  262. image_group_data = array.array("H", data)
  263. true_channels_no = int(len(image_group_data) / (h * w))
  264. image_data = np.reshape(image_group_data, (h, w, true_channels_no))
  265. missing_channels = ~np.any(image_data, axis=(0, 1))
  266. image_data[..., missing_channels] = np.full(
  267. (h, w, missing_channels.sum()), np.nan)
  268. if np.any(missing_channels):
  269. warnings.warn(
  270. "ND2 file contains gap frames which are represented by "
  271. + "np.nan-filled arrays; to convert to zeros use e.g. "
  272. + "np.nan_to_num(array)")
  273. return timestamp, image_data[...,0]
  274. def _get_raw_image_data(self, image_group_number, channel_offset, height, width):
  275. """Reads the raw bytes and the timestamp of an image.
  276. Args:
  277. image_group_number: the image group number (see _calculate_image_group_number)
  278. channel_offset: the number of the color channel
  279. height: the height of the image
  280. width: the width of the image
  281. Returns:
  282. """
  283. chunk = self._label_map.get_image_data_location(image_group_number)
  284. data = read_chunk(self._fh, chunk)
  285. # All images in the same image group share the same timestamp! So if you have complicated image data,
  286. # your timestamps may not be entirely accurate. Practically speaking though, they'll only be off by a few
  287. # seconds unless you're doing something super weird.
  288. timestamp = struct.unpack("d", data[:8])[0]
  289. image_group_data = array.array("H", data)
  290. image_data_start = 4 + channel_offset
  291. image_group_data = stitched.remove_parsed_unwanted_bytes(image_group_data, image_data_start, height, width)
  292. # The images for the various channels are interleaved within the same array. For example, the second image
  293. # of a four image group will be composed of bytes 2, 6, 10, etc. If you understand why someone would design
  294. # a data structure that way, please send the author of this library a message.
  295. number_of_true_channels = int(len(image_group_data[4:]) / (height * width))
  296. try:
  297. image_data = np.reshape(image_group_data[image_data_start::number_of_true_channels], (height, width))
  298. except ValueError:
  299. new_width = len(image_group_data[image_data_start::number_of_true_channels]) // height
  300. image_data = np.reshape(image_group_data[image_data_start::number_of_true_channels], (height, new_width))
  301. # Skip images that are all zeros! This is important, since NIS Elements creates blank "gap" images if you
  302. # don't have the same number of images each cycle. We discovered this because we only took GFP images every
  303. # other cycle to reduce phototoxicity, but NIS Elements still allocated memory as if we were going to take
  304. # them every cycle.
  305. if np.any(image_data):
  306. return timestamp, image_data
  307. # If a blank "gap" image is encountered, generate an array of corresponding height and width to avoid
  308. # errors with ND2-files with missing frames. Array is filled with nan to reflect that data is missing.
  309. else:
  310. empty_frame = np.full((height, width), np.nan)
  311. warnings.warn(
  312. "ND2 file contains gap frames which are represented by np.nan-filled arrays; to convert to zeros use e.g. np.nan_to_num(array)")
  313. return timestamp, image_data
  314. def _get_frame_metadata(self):
  315. """Get the metadata for one frame
  316. Returns:
  317. dict: a dictionary containing the parsed metadata
  318. """
  319. return self.metadata