From cc2a49ac6dc7f2df1e0c593476a63d07d73cae94 Mon Sep 17 00:00:00 2001 From: jim Date: Sat, 31 Oct 2015 23:28:53 -0500 Subject: [PATCH 1/2] #110 added many comments, fixed parameter names where frame_number was incorrectly used in place of index --- nd2reader/common/v3.py | 6 +++- nd2reader/driver/v3.py | 72 +++++++++++++++++++++++++++++++++++------- nd2reader/interface.py | 43 ++++++++++++++++++++++--- 3 files changed, 105 insertions(+), 16 deletions(-) diff --git a/nd2reader/common/v3.py b/nd2reader/common/v3.py index 2c33c7e..955dcc9 100644 --- a/nd2reader/common/v3.py +++ b/nd2reader/common/v3.py @@ -3,7 +3,11 @@ import struct def read_chunk(fh, chunk_location): """ - Gets the data for a given chunk pointer + Reads a piece of data given the location of its pointer. + + :param fh: an open file handle to the ND2 + :param chunk_location: a pointer + :type chunk_location: int :rtype: bytes diff --git a/nd2reader/driver/v3.py b/nd2reader/driver/v3.py index fc17694..156e886 100644 --- a/nd2reader/driver/v3.py +++ b/nd2reader/driver/v3.py @@ -10,38 +10,88 @@ from nd2reader.exc import NoImageError class V3Driver(object): + """ + Accesses images from ND2 files made with NIS Elements 4.x. Confusingly, files of this type have a version number of 3.0+. + + """ def __init__(self, metadata, label_map, file_handle): + """ + :param metadata: a Metadata object + :param label_map: a raw dictionary of pointers to image locations + :param file_handle: an open file handle to the ND2 + + """ self._metadata = metadata self._label_map = label_map self._file_handle = file_handle - def _calculate_field_of_view(self, frame_number): + def _calculate_field_of_view(self, index): + """ + Determines what field of view was being imaged for a given image. + + :type index: int + :rtype: int + + """ images_per_cycle = len(self._metadata.z_levels) * len(self._metadata.channels) - return int((frame_number - (frame_number % images_per_cycle)) / images_per_cycle) % len(self._metadata.fields_of_view) + return int((index - (index % images_per_cycle)) / images_per_cycle) % len(self._metadata.fields_of_view) + + def _calculate_channel(self, index): + """ + Determines what channel a particular image is. + + :type index: int + :rtype: str + + """ + return self._metadata.channels[index % len(self._metadata.channels)] - def _calculate_channel(self, frame_number): - return self._metadata.channels[frame_number % len(self._metadata.channels)] + def _calculate_z_level(self, index): + """ + Determines the plane in the z-axis a given image was taken in. In the future, this will be replaced with the actual offset in micrometers. - def _calculate_z_level(self, frame_number): - return self._metadata.z_levels[int(((frame_number - (frame_number % len(self._metadata.channels))) / len(self._metadata.channels)) % len(self._metadata.z_levels))] + :type index: int + :rtype: int + """ + return self._metadata.z_levels[int(((index - (index % len(self._metadata.channels))) / len(self._metadata.channels)) % len(self._metadata.z_levels))] - def _calculate_image_group_number(self, time_index, fov, z_level): + def _calculate_image_group_number(self, frame_number, fov, z_level): """ Images are grouped together if they share the same time index, field of view, and z-level. - :type time_index: int + :type frame_number: int :type fov: int :type z_level: int :rtype: int """ - return time_index * len(self._metadata.fields_of_view) * len(self._metadata.z_levels) + (fov * len(self._metadata.z_levels) + z_level) + return frame_number * len(self._metadata.fields_of_view) * len(self._metadata.z_levels) + (fov * len(self._metadata.z_levels) + z_level) + + def _calculate_frame_number(self, image_group_number, field_of_view, z_level): + """ + Images are in the same frame if they share the same group number and field of view and are taken sequentially. + + :type image_group_number: int + :type field_of_view: int + :type z_level: int - def _calculate_frame_number(self, image_group_number, fov, z_level): - return (image_group_number - (fov * len(self._metadata.z_levels) + z_level)) / (len(self._metadata.fields_of_view) * len(self._metadata.z_levels)) + :rtype: int + + """ + 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)) def get_image(self, index): + """ + Creates an Image object and adds its metadata, based on the index, which is simply the order in which the image was acquired. May return None if the ND2 contains + multiple channels and not all were taken in each cycle (for example, if you take bright field images every minute, and GFP images every five minutes, there will be some + indexes that do not contain an image. The reason for this is complicated, but suffice it to say that we hope to eliminate this possibility in future releases. For now, + you'll need to check if your image is None if you're doing anything as described above. + + :type index: int + :rtype: Image or None + + """ channel_offset = index % len(self._metadata.channels) fov = self._calculate_field_of_view(index) channel = self._calculate_channel(index) diff --git a/nd2reader/interface.py b/nd2reader/interface.py index 6e3ca5a..5591e33 100644 --- a/nd2reader/interface.py +++ b/nd2reader/interface.py @@ -82,28 +82,62 @@ class Nd2(object): @property def date(self): + """ + The date and time that the acquisition began. Not guaranteed to have been recorded. + + :rtype: datetime.datetime() or None + + """ return self._metadata.date @property def z_levels(self): + """ + A list of integers that represent the different levels on the Z-axis that images were taken. Currently this is 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 set position would be represented by 0, 1 and 2, respectively. ND2s do store the actual + offset of each image in micrometers and in the future this will hopefully be available. For now, however, you will have to match up the order yourself. + + :return: list of int + + """ return self._metadata.z_levels @property def fields_of_view(self): + """ + A list of integers representing the various stage locations, in the order they were taken in the first round of acquisition. + + :return: list of int + + """ return self._metadata.fields_of_view @property def channels(self): + """ + A list of channel (i.e. wavelength) names. These are set by the user in NIS Elements. + + :return: list of str + + """ return self._metadata.channels @property def frames(self): + """ + A list of integers representing groups of images. ND2s consider images to be part of the same frame if they 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 four different fields of view over and over again, you'll have 8 images and 4 frames per cycle. + + :return: list of int + + """ return self._metadata.frames @property def height(self): """ - :return: height of each image, in pixels + The height of each image in pixels. + :rtype: int """ @@ -112,7 +146,8 @@ class Nd2(object): @property def width(self): """ - :return: width of each image, in pixels + The width of each image in pixels. + :rtype: int """ @@ -120,7 +155,7 @@ class Nd2(object): def get_image(self, frame_number, field_of_view, channel_name, z_level): """ - Returns an Image if data exists for the given parameters, otherwise returns None. + Attempts to return the image with the unique combination of given attributes. None will be returned if a match is not found. :type frame_number: int :param field_of_view: the label for the place in the XY-plane where this image was taken. @@ -130,7 +165,7 @@ class Nd2(object): :param z_level: the label for the location in the Z-plane where this image was taken. :type z_level: int - :rtype: nd2reader.model.Image() + :rtype: nd2reader.model.Image() or None """ return self._driver.get_image_by_attributes(frame_number, field_of_view, channel_name, z_level, self.height, self.width) From 09173fcb9e4f2bc4dcbdb9d484c039c82ed549db Mon Sep 17 00:00:00 2001 From: jim Date: Sun, 1 Nov 2015 00:33:11 -0500 Subject: [PATCH 2/2] resolves #110: added comments and parameter types to every method with a few minor exceptions where it's super obvious and unimportant what's happening --- nd2reader/driver/v3.py | 34 +++++++++++++++++++----------- nd2reader/interface.py | 42 +++++++++++++++++++------------------ nd2reader/model/image.py | 39 ++++++++++++++++++++++++++-------- nd2reader/model/metadata.py | 6 ++++++ nd2reader/parser/base.py | 8 +++++++ nd2reader/parser/parser.py | 10 +++++++++ nd2reader/parser/v3.py | 33 ++++++++++++++++++++++++----- 7 files changed, 126 insertions(+), 46 deletions(-) diff --git a/nd2reader/driver/v3.py b/nd2reader/driver/v3.py index 156e886..29746ae 100644 --- a/nd2reader/driver/v3.py +++ b/nd2reader/driver/v3.py @@ -83,27 +83,27 @@ class V3Driver(object): def get_image(self, index): """ - Creates an Image object and adds its metadata, based on the index, which is simply the order in which the image was acquired. May return None if the ND2 contains + Creates an Image object and adds its metadata, based on the index (which is simply the order in which the image was acquired). May return None if the ND2 contains multiple channels and not all were taken in each cycle (for example, if you take bright field images every minute, and GFP images every five minutes, there will be some indexes that do not contain an image. The reason for this is complicated, but suffice it to say that we hope to eliminate this possibility in future releases. For now, - you'll need to check if your image is None if you're doing anything as described above. + you'll need to check if your image is None if you're doing anything out of the ordinary. :type index: int :rtype: Image or None """ channel_offset = index % len(self._metadata.channels) - fov = self._calculate_field_of_view(index) + field_of_view = self._calculate_field_of_view(index) channel = self._calculate_channel(index) z_level = self._calculate_z_level(index) image_group_number = int(index / len(self._metadata.channels)) - frame_number = self._calculate_frame_number(image_group_number, fov, z_level) + frame_number = self._calculate_frame_number(image_group_number, field_of_view, z_level) try: timestamp, image = self._get_raw_image_data(image_group_number, channel_offset, self._metadata.height, self._metadata.width) except NoImageError: return None else: - image.add_params(timestamp, frame_number, fov, channel, z_level) + image.add_params(timestamp, frame_number, field_of_view, channel, z_level) return image @property @@ -115,21 +115,19 @@ class V3Driver(object): :rtype: dict """ - channel_offset = {} - for n, channel in enumerate(self._metadata.channels): - channel_offset[channel] = n - return channel_offset + return {channel: n for n, channel in enumerate(self._metadata.channels)} def _get_raw_image_data(self, image_group_number, channel_offset, height, width): """ Reads the raw bytes and the timestamp of an image. - :param image_group_number: groups are made of images with the same time index, field of view and z-level. + :param image_group_number: groups are made of images with the same time index, field of view and z-level :type image_group_number: int - :param channel_offset: the offset in the array where the bytes for this image are found. + :param channel_offset: the offset in the array where the bytes for this image are found :type channel_offset: int - :return: (int, array.array()) or None + :rtype: (int, Image) + :raises: NoImageError """ chunk = self._label_map[six.b("ImageDataSeq|%d!" % image_group_number)] @@ -153,6 +151,18 @@ class V3Driver(object): raise NoImageError def get_image_by_attributes(self, frame_number, field_of_view, channel_name, z_level, height, width): + """ + Attempts to get Image based on attributes alone. + + :type frame_number: int + :type field_of_view: int + :type channel_name: str + :type z_level: int + :type height: int + :type width: int + + :rtype: Image or None + """ image_group_number = self._calculate_image_group_number(frame_number, field_of_view, z_level) try: timestamp, raw_image_data = self._get_raw_image_data(image_group_number, diff --git a/nd2reader/interface.py b/nd2reader/interface.py index 5591e33..6b584a6 100644 --- a/nd2reader/interface.py +++ b/nd2reader/interface.py @@ -5,10 +5,8 @@ from nd2reader.version import get_version class Nd2(object): - """ - Allows easy access to NIS Elements .nd2 image files. + """ Allows easy access to NIS Elements .nd2 image files. """ - """ def __init__(self, filename): self._filename = filename self._fh = open(filename, "rb") @@ -39,7 +37,7 @@ class Nd2(object): This should be the total number of images in the ND2, but it may be inaccurate. If the ND2 contains a different number of images in a cycle (i.e. there are "gap" images) it will be higher than reality. - :rtype: int + :rtype: int """ return self._metadata.total_images_per_channel * len(self.channels) @@ -48,8 +46,8 @@ class Nd2(object): """ Allows slicing ND2s. - :type item: int or slice - :rtype: nd2reader.model.Image() or generator + :type item: int or slice + :rtype: nd2reader.model.Image() or generator """ if isinstance(item, int): @@ -67,10 +65,10 @@ class Nd2(object): """ Allows for iteration over a selection of the entire dataset. - :type start: int - :type stop: int - :type step: int - :rtype: nd2reader.model.Image() + :type start: int + :type stop: int + :type step: int + :rtype: nd2reader.model.Image() """ start = start if start is not None else 0 @@ -85,7 +83,7 @@ class Nd2(object): """ The date and time that the acquisition began. Not guaranteed to have been recorded. - :rtype: datetime.datetime() or None + :rtype: datetime.datetime() or None """ return self._metadata.date @@ -138,7 +136,7 @@ class Nd2(object): """ The height of each image in pixels. - :rtype: int + :rtype: int """ return self._metadata.height @@ -148,7 +146,7 @@ class Nd2(object): """ The width of each image in pixels. - :rtype: int + :rtype: int """ return self._metadata.width @@ -157,13 +155,13 @@ class Nd2(object): """ Attempts to return the image with the unique combination of given attributes. None will be returned if a match is not found. - :type frame_number: int - :param field_of_view: the label for the place in the XY-plane where this image was taken. - :type field_of_view: int - :param channel_name: the name of the color of this image - :type channel_name: str - :param z_level: the label for the location in the Z-plane where this image was taken. - :type z_level: int + :type frame_number: int + :param field_of_view: the label for the place in the XY-plane where this image was taken. + :type field_of_view: int + :param channel_name: the name of the color of this image + :type channel_name: str + :param z_level: the label for the location in the Z-plane where this image was taken. + :type z_level: int :rtype: nd2reader.model.Image() or None @@ -171,4 +169,8 @@ class Nd2(object): return self._driver.get_image_by_attributes(frame_number, field_of_view, channel_name, z_level, self.height, self.width) def close(self): + """ + Closes the file handle to the image. This actually sometimes will prevent problems so it's good to do this or use Nd2 as a context manager. + + """ self._fh.close() diff --git a/nd2reader/model/image.py b/nd2reader/model/image.py index 62ffea3..75e6beb 100644 --- a/nd2reader/model/image.py +++ b/nd2reader/model/image.py @@ -4,6 +4,10 @@ import numpy as np class Image(np.ndarray): + """ + Holds the raw pixel data of an image and provides access to some metadata. + + """ def __new__(cls, array): return np.asarray(array).view(cls) @@ -16,8 +20,6 @@ class Image(np.ndarray): def add_params(self, timestamp, frame_number, field_of_view, channel, z_level): """ - A wrapper around the raw pixel data of an image. - :param timestamp: The number of milliseconds after the beginning of the acquisition that this image was taken. :type timestamp: float :param frame_number: The order in which this image was taken, with images of different channels/z-levels @@ -39,18 +41,30 @@ class Image(np.ndarray): @property def height(self): + """ + The height in pixels. + + :rtype: int + + """ return self.shape[0] @property def width(self): + """ + The width in pixels. + + :rtype: int + + """ return self.shape[1] @property def field_of_view(self): """ - Which of the fixed locations this image was taken at. + The index of the stage location where this image was acquired. - :rtype int: + :rtype: int """ return self._field_of_view @@ -60,16 +74,23 @@ class Image(np.ndarray): """ The number of seconds after the beginning of the acquisition that the image was taken. Note that for a given field of view and z-level offset, if you have images of multiple channels, they will all be given the same - timestamp. No, this doesn't make much sense. But that's how ND2s are structured, so if your experiment depends - on millisecond accuracy, you need to find an alternative imaging system. + timestamp. That's just how ND2s are structured, so if your experiment depends on millisecond accuracy, + you need to find an alternative imaging system. - :rtype float: + :rtype: float """ + # data is actually stored in milliseconds return self._timestamp / 1000.0 @property def frame_number(self): + """ + The index of the group of images taken sequentially that all have the same group number and field of view. + + :rtype: int + + """ return self._frame_number @property @@ -77,7 +98,7 @@ class Image(np.ndarray): """ The name of the filter used to acquire this image. These are user-supplied in NIS Elements. - :rtype str: + :rtype: str """ return self._channel @@ -94,7 +115,7 @@ class Image(np.ndarray): 0 µm: 1 +3 µm: 2 - :rtype int: + :rtype: int """ return self._z_level diff --git a/nd2reader/model/metadata.py b/nd2reader/model/metadata.py index 536f0b9..6e4f57a 100644 --- a/nd2reader/model/metadata.py +++ b/nd2reader/model/metadata.py @@ -86,4 +86,10 @@ class Metadata(object): @property def total_images_per_channel(self): + """ + The total number of images of a particular channel (wavelength, filter, etc) in the entire ND2. + + :rtype: int + + """ return self._total_images_per_channel diff --git a/nd2reader/parser/base.py b/nd2reader/parser/base.py index 20b40d1..07fef59 100644 --- a/nd2reader/parser/base.py +++ b/nd2reader/parser/base.py @@ -4,8 +4,16 @@ from abc import abstractproperty class BaseParser(object): @abstractproperty def metadata(self): + """ + Instantiates a Metadata object. + + """ raise NotImplementedError @abstractproperty def driver(self): + """ + Instantiates a driver object. + + """ raise NotImplementedError diff --git a/nd2reader/parser/parser.py b/nd2reader/parser/parser.py index 062c835..d2027d5 100644 --- a/nd2reader/parser/parser.py +++ b/nd2reader/parser/parser.py @@ -3,6 +3,16 @@ from nd2reader.exc import InvalidVersionError def get_parser(fh, major_version, minor_version): + """ + Picks the appropriate parser based on the ND2 version. + + :type fh: file + :type major_version: int + :type minor_version: int + + :rtype: a parser object + + """ parsers = {(3, None): V3Parser} parser = parsers.get((major_version, minor_version)) or parsers.get((major_version, None)) if not parser: diff --git a/nd2reader/parser/v3.py b/nd2reader/parser/v3.py index 2500dfc..147902a 100644 --- a/nd2reader/parser/v3.py +++ b/nd2reader/parser/v3.py @@ -12,18 +12,26 @@ import struct class V3Parser(BaseParser): - """ Parses ND2 files and creates a Metadata and ImageReader object. """ + """ Parses ND2 files and creates a Metadata and driver object. """ CHUNK_HEADER = 0xabeceda CHUNK_MAP_START = six.b("ND2 FILEMAP SIGNATURE NAME 0001!") CHUNK_MAP_END = six.b("ND2 CHUNK MAP SIGNATURE 0000001!") def __init__(self, fh): + """ + :type fh: file + + """ self._fh = fh self._metadata = None self._label_map = None @property def metadata(self): + """ + :rtype: Metadata + + """ if not self._metadata: self._parse_metadata() return self._metadata @@ -34,7 +42,7 @@ class V3Parser(BaseParser): def _parse_metadata(self): """ - Reads all metadata. + Reads all metadata and instantiates the Metadata object. """ metadata_dict = {} @@ -59,6 +67,7 @@ class V3Parser(BaseParser): """ The date and time when acquisition began. + :type metadata_dict: dict :rtype: datetime.datetime() or None """ @@ -85,6 +94,7 @@ class V3Parser(BaseParser): These are labels created by the NIS Elements user. Typically they may a short description of the filter cube used (e.g. "bright field", "GFP", etc.) + :type metadata_dict: dict :rtype: list """ @@ -111,7 +121,8 @@ class V3Parser(BaseParser): in the image data, so we have to calculate it. There probably is something somewhere, since NIS Elements can figure it out, but we haven't found it yet. - :rtype: list + :type metadata_dict: dict + :rtype: list """ return self._parse_dimension(r""".*?XY\((\d+)\).*?""", metadata_dict) @@ -120,6 +131,7 @@ class V3Parser(BaseParser): """ The number of cycles. + :type metadata_dict: dict :rtype: list """ @@ -129,7 +141,8 @@ class V3Parser(BaseParser): """ The different levels in the Z-plane. Just a sequence from 0 to n. - :rtype: list + :type metadata_dict: dict + :rtype: list """ return self._parse_dimension(r""".*?Z\((\d+)\).*?""", metadata_dict) @@ -140,7 +153,8 @@ class V3Parser(BaseParser): Sometimes certain elements don't exist, or change their data type randomly. However, the human-readable text is always there and in the same exact format, so we just parse that instead. - :rtype: str + :type metadata_dict: dict + :rtype: str """ for line in metadata_dict[six.b('ImageTextInfo')][six.b('SLxImageTextInfo')].values(): @@ -158,6 +172,14 @@ class V3Parser(BaseParser): return dimension_text def _parse_dimension(self, pattern, metadata_dict): + """ + :param pattern: a valid regex pattern + :type pattern: str + :type metadata_dict: dict + + :rtype: list of int + + """ dimension_text = self._parse_dimension_text(metadata_dict) if six.PY3: dimension_text = dimension_text.decode("utf8") @@ -171,6 +193,7 @@ class V3Parser(BaseParser): """ The total number of images per channel. Warning: this may be inaccurate as it includes "gap" images. + :type metadata_dict: dict :rtype: int """