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.

249 lines
6.6 KiB

4 years ago
4 years ago
7 years ago
7 years ago
6 years ago
  1. from pims import Frame
  2. from pims.base_frames import FramesSequenceND
  3. from nd2reader2.exceptions import EmptyFileError, InvalidFileType
  4. from nd2reader2.parser import Parser
  5. import numpy as np
  6. class ND2Reader(FramesSequenceND):
  7. """PIMS wrapper for the ND2 parser.
  8. This is the main class: use this to process your .nd2 files.
  9. """
  10. _fh = None
  11. class_priority = 12
  12. def __init__(self, fh):
  13. """
  14. Arguments:
  15. fh {str} -- absolute path to .nd2 file
  16. fh {IO} -- input buffer handler (opened with "rb" mode)
  17. """
  18. super(ND2Reader, self).__init__()
  19. self.filename = ""
  20. if isinstance(fh, str):
  21. if not fh.endswith(".nd2"):
  22. raise InvalidFileType(
  23. ("The file %s you want to read with nd2reader" % fh)
  24. + " does not have extension .nd2."
  25. )
  26. self.filename = fh
  27. fh = open(fh, "rb")
  28. self._fh = fh
  29. self._parser = Parser(self._fh)
  30. # Setup metadata
  31. self.metadata = self._parser.metadata
  32. # Set data type
  33. self._dtype = self._parser.get_dtype_from_metadata()
  34. # Setup the axes
  35. self._setup_axes()
  36. # Other properties
  37. self._timesteps = None
  38. @classmethod
  39. def class_exts(cls):
  40. """Let PIMS open function use this reader for opening .nd2 files
  41. """
  42. return {"nd2"} | super(ND2Reader, cls).class_exts()
  43. def close(self):
  44. """Correctly close the file handle
  45. """
  46. if self._fh is not None:
  47. self._fh.close()
  48. def _get_default(self, coord):
  49. try:
  50. return self.default_coords[coord]
  51. except KeyError:
  52. return 0
  53. def get_roi(self, roi, c=0, t=0, z=0, x=0, y=0, v=0):
  54. height = self.metadata['height']
  55. width = self.metadata['width']
  56. ylim = roi[0].indices(height)
  57. xlim = roi[1].indices(width)
  58. y = ylim[0]
  59. x = xlim[0]
  60. w = xlim[1]-xlim[0]
  61. h = ylim[1]-ylim[0]
  62. return self._parser.get_slice_by_attributes(
  63. (x, y, w, h), t, v, c, z, height, width
  64. )
  65. def get_frame_2D(self, c=0, t=0, z=0, x=0, y=0, v=0):
  66. """Gets a given frame using the parser
  67. Args:
  68. x: The x-index (pims expects this)
  69. y: The y-index (pims expects this)
  70. c: The color channel number
  71. t: The frame number
  72. z: The z stack number
  73. v: The field of view index
  74. Returns:
  75. pims.Frame: The requested frame
  76. """
  77. # This needs to be set to width/height to return an image
  78. x = self.metadata["width"]
  79. y = self.metadata["height"]
  80. return self._parser.get_image_by_attributes(t, v, c, z, y, x)
  81. @property
  82. def parser(self):
  83. """
  84. Returns the parser object.
  85. Returns:
  86. Parser: the parser object
  87. """
  88. return self._parser
  89. @property
  90. def pixel_type(self):
  91. """Return the pixel data type
  92. Returns:
  93. dtype: the pixel data type
  94. """
  95. return self._dtype
  96. @property
  97. def timesteps(self):
  98. """Get the timesteps of the experiment
  99. Returns:
  100. np.ndarray: an array of times in milliseconds.
  101. """
  102. if self._timesteps is None:
  103. return self.get_timesteps()
  104. return self._timesteps
  105. @property
  106. def events(self):
  107. """Get the events of the experiment
  108. Returns:
  109. iterator of events as dict
  110. """
  111. return self._get_metadata_property("events")
  112. @property
  113. def frame_rate(self):
  114. """The (average) frame rate
  115. Returns:
  116. float: the (average) frame rate in frames per second
  117. """
  118. total_duration = 0.0
  119. for loop in self.metadata["experiment"]["loops"]:
  120. total_duration += loop["duration"]
  121. if total_duration == 0:
  122. total_duration = self.timesteps[-1]
  123. if total_duration == 0:
  124. raise ValueError(
  125. "Total measurement duration could not be determined from loops"
  126. )
  127. return self.metadata["num_frames"] / (total_duration / 1000.0)
  128. def _get_metadata_property(self, key, default=None):
  129. if self.metadata is None:
  130. return default
  131. if key not in self.metadata:
  132. return default
  133. if self.metadata[key] is None:
  134. return default
  135. return self.metadata[key]
  136. def _setup_axes(self):
  137. """Setup the xyctz axes, iterate over t axis by default
  138. """
  139. self._init_axis_if_exists("x", self._get_metadata_property("width", default=0))
  140. self._init_axis_if_exists("y", self._get_metadata_property("height", default=0))
  141. self._init_axis_if_exists(
  142. "c", len(self._get_metadata_property("channels", default=[])), min_size=2
  143. )
  144. self._init_axis_if_exists(
  145. "t", len(self._get_metadata_property("frames", default=[]))
  146. )
  147. self._init_axis_if_exists(
  148. "z", len(self._get_metadata_property("z_levels", default=[])), min_size=2
  149. )
  150. self._init_axis_if_exists(
  151. "v",
  152. len(self._get_metadata_property("fields_of_view", default=[])),
  153. min_size=2,
  154. )
  155. if len(self.sizes) == 0:
  156. raise EmptyFileError("No axes were found for this .nd2 file.")
  157. # provide the default
  158. self.iter_axes = self._guess_default_iter_axis()
  159. self._register_get_frame(self.get_frame_2D, "yx")
  160. def _init_axis_if_exists(self, axis, size, min_size=1):
  161. if size >= min_size:
  162. self._init_axis(axis, size)
  163. def _guess_default_iter_axis(self):
  164. """
  165. Guesses the default axis to iterate over based on axis sizes.
  166. Returns:
  167. the axis to iterate over
  168. """
  169. priority = ["t", "z", "c", "v"]
  170. found_axes = []
  171. for axis in priority:
  172. try:
  173. current_size = self.sizes[axis]
  174. except KeyError:
  175. continue
  176. if current_size > 1:
  177. return axis
  178. found_axes.append(axis)
  179. return found_axes[0]
  180. def get_timesteps(self):
  181. """Get the timesteps of the experiment
  182. Returns:
  183. np.ndarray: an array of times in milliseconds.
  184. """
  185. if self._timesteps is not None and len(self._timesteps) > 0:
  186. return self._timesteps
  187. self._timesteps = (
  188. np.array(list(self._parser._raw_metadata.acquisition_times), dtype=np.float)
  189. * 1000.0
  190. )
  191. return self._timesteps