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.

237 lines
8.3 KiB

9 years ago
9 years ago
9 years ago
  1. # -*- coding: utf-8 -*-
  2. from nd2reader.parser import get_parser
  3. from nd2reader.version import get_version
  4. import six
  5. class Nd2(object):
  6. """ Allows easy access to NIS Elements .nd2 image files. """
  7. def __init__(self, filename):
  8. self._filename = filename
  9. self._fh = open(filename, "rb")
  10. major_version, minor_version = get_version(self._fh)
  11. self._parser = get_parser(self._fh, major_version, minor_version)
  12. self._metadata = self._parser.metadata
  13. self._roi_metadata = self._parser.roi_metadata
  14. def __repr__(self):
  15. return "\n".join(["<ND2 %s>" % self._filename,
  16. "Created: %s" % (self.date if self.date is not None else "Unknown"),
  17. "Image size: %sx%s (HxW)" % (self.height, self.width),
  18. "Frames: %s" % len(self.frames),
  19. "Channels: %s" % ", ".join(["%s" % str(channel) for channel in self.channels]),
  20. "Fields of View: %s" % len(self.fields_of_view),
  21. "Z-Levels: %s" % len(self.z_levels)
  22. ])
  23. def __enter__(self):
  24. return self
  25. def __exit__(self, exc_type, exc_val, exc_tb):
  26. if self._fh is not None:
  27. self._fh.close()
  28. def __len__(self):
  29. """
  30. This should be the total number of images in the ND2, but it may be inaccurate. If the ND2 contains a
  31. different number of images in a cycle (i.e. there are "gap" images) it will be higher than reality.
  32. :rtype: int
  33. """
  34. return self._metadata.total_images_per_channel * len(self.channels)
  35. def __getitem__(self, item):
  36. """
  37. Allows slicing ND2s.
  38. :type item: int or slice
  39. :rtype: nd2reader.model.Image() or generator
  40. """
  41. if isinstance(item, int):
  42. try:
  43. image = self._parser.driver.get_image(item)
  44. except KeyError:
  45. raise IndexError
  46. else:
  47. return image
  48. elif isinstance(item, slice):
  49. return self._slice(item.start, item.stop, item.step)
  50. raise IndexError
  51. def select(self, fields_of_view=None, channels=None, z_levels=None, start=0, stop=None):
  52. """
  53. Iterates over images matching the given criteria. This can be 2-10 times faster than manually iterating over
  54. the Nd2 and checking the attributes of each image, as this method skips disk reads for any images that don't
  55. meet the criteria.
  56. :type fields_of_view: int or tuple or list
  57. :type channels: str or tuple or list
  58. :type z_levels: int or tuple or list
  59. :type start: int
  60. :type stop: int
  61. """
  62. fields_of_view = self._to_tuple(fields_of_view, self.fields_of_view)
  63. channels = self._to_tuple(channels, self.channels)
  64. z_levels = self._to_tuple(z_levels, self.z_levels)
  65. # By default, we stop after the last image. Otherwise we make sure the user-provided value is valid
  66. stop = len(self) if stop is None else max(0, min(stop, len(self)))
  67. for frame in range(start, stop):
  68. field_of_view, channel, z_level = self._parser.driver.calculate_image_properties(frame)
  69. if field_of_view in fields_of_view and channel in channels and z_level in z_levels:
  70. image = self._parser.driver.get_image(frame)
  71. if image is not None:
  72. yield image
  73. @property
  74. def height(self):
  75. """
  76. The height of each image in pixels.
  77. :rtype: int
  78. """
  79. return self._metadata.height
  80. @property
  81. def width(self):
  82. """
  83. The width of each image in pixels.
  84. :rtype: int
  85. """
  86. return self._metadata.width
  87. @property
  88. def z_levels(self):
  89. """
  90. A list of integers that represent the different levels on the Z-axis that images were taken. Currently this is
  91. just a list of numbers from 0 to N. For example, an ND2 where images were taken at -3µm, 0µm, and +5µm from a
  92. set position would be represented by 0, 1 and 2, respectively. ND2s do store the actual offset of each image
  93. in micrometers and in the future this will hopefully be available. For now, however, you will have to match up
  94. the order yourself.
  95. :return: list of int
  96. """
  97. return self._metadata.z_levels
  98. @property
  99. def fields_of_view(self):
  100. """
  101. A list of integers representing the various stage locations, in the order they were taken in the first round
  102. of acquisition.
  103. :return: list of int
  104. """
  105. return self._metadata.fields_of_view
  106. @property
  107. def channels(self):
  108. """
  109. A list of channel (i.e. wavelength) names. These are set by the user in NIS Elements.
  110. :return: list of str
  111. """
  112. return self._metadata.channels
  113. @property
  114. def frames(self):
  115. """
  116. A list of integers representing groups of images. ND2s consider images to be part of the same frame if they
  117. are in the same field of view and don't have the same channel. So if you take a bright field and GFP image at
  118. four different fields of view over and over again, you'll have 8 images and 4 frames per cycle.
  119. :return: list of int
  120. """
  121. return self._metadata.frames
  122. @property
  123. def date(self):
  124. """
  125. The date and time that the acquisition began. Not guaranteed to have been recorded.
  126. :rtype: datetime.datetime() or None
  127. """
  128. return self._metadata.date
  129. @property
  130. def pixel_microns(self):
  131. """
  132. The width of a pixel in microns. Note that the user can override this in NIS Elements so it may not reflect reality.
  133. :rtype: float
  134. """
  135. return self._metadata.pixel_microns
  136. def get_image(self, frame_number, field_of_view, channel_name, z_level):
  137. """
  138. Attempts to return the image with the unique combination of given attributes. None will be returned if a match
  139. is not found.
  140. :type frame_number: int
  141. :param field_of_view: the label for the place in the XY-plane where this image was taken.
  142. :type field_of_view: int
  143. :param channel_name: the name of the color of this image
  144. :type channel_name: str
  145. :param z_level: the label for the location in the Z-plane where this image was taken.
  146. :type z_level: int
  147. :rtype: nd2reader.model.Image() or None
  148. """
  149. return self._parser.driver.get_image_by_attributes(frame_number,
  150. field_of_view,
  151. channel_name,
  152. z_level,
  153. self.height,
  154. self.width)
  155. def close(self):
  156. """
  157. Closes the file handle to the image. This actually sometimes will prevent problems so it's good to do this or
  158. use Nd2 as a context manager.
  159. """
  160. self._fh.close()
  161. def _slice(self, start, stop, step):
  162. """
  163. Allows for iteration over a selection of the entire dataset.
  164. :type start: int
  165. :type stop: int
  166. :type step: int
  167. :rtype: nd2reader.model.Image()
  168. """
  169. start = start if start is not None else 0
  170. step = step if step is not None else 1
  171. stop = stop if stop is not None else len(self)
  172. # This weird thing with the step allows you to iterate backwards over the images
  173. for i in range(start, stop)[::step]:
  174. yield self[i]
  175. def _to_tuple(self, value, default):
  176. """
  177. Idempotently converts a value to a tuple. This allows users to pass in scalar values and iterables to
  178. select(), which is more ergonomic than having to remember to pass in single-member lists
  179. :type value: int or str or tuple or list
  180. :type default: tuple or list
  181. :rtype: tuple
  182. """
  183. value = default if value is None else value
  184. return (value,) if isinstance(value, int) or isinstance(value, six.string_types) else tuple(value)