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.

213 lines
5.9 KiB

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