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.

232 lines
6.2 KiB

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