diff --git a/nd2reader/__init__.py b/nd2reader/__init__.py index cf19770..d845fad 100644 --- a/nd2reader/__init__.py +++ b/nd2reader/__init__.py @@ -1,152 +1 @@ -# -*- coding: utf-8 -*- - -from nd2reader.model import Image, ImageSet -from nd2reader.parser import Nd2Parser -import six - - -class Nd2(Nd2Parser): - """ - Allows easy access to NIS Elements .nd2 image files. - - """ - def __init__(self, filename): - super(Nd2, self).__init__(filename) - self._filename = filename - - def __repr__(self): - return "\n".join(["" % self._filename, - "Created: %s" % self.absolute_start.strftime("%Y-%m-%d %H:%M:%S"), - "Image size: %sx%s (HxW)" % (self.height, self.width), - "Image cycles: %s" % len(self.time_indexes), - "Channels: %s" % ", ".join(["'%s'" % str(channel) for channel in self.channels]), - "Fields of View: %s" % len(self.fields_of_view), - "Z-Levels: %s" % len(self.z_levels) - ]) - - def __len__(self): - """ - 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 - - """ - return self._total_images_per_channel * len(self.channels) - - def __getitem__(self, item): - """ - Allows slicing ND2s. - - >>> nd2 = Nd2("my_images.nd2") - >>> image = nd2[16] # gets 17th frame - >>> for image in nd2[100:200]: # iterate over the 100th to 200th images - >>> do_something(image.data) - >>> for image in nd2[::-1]: # iterate backwards - >>> do_something(image.data) - >>> for image in nd2[37:422:17]: # do something super weird if you really want to - >>> do_something(image.data) - - :type item: int or slice - :rtype: nd2reader.model.Image() or generator - - """ - if isinstance(item, int): - try: - channel_offset = item % len(self.channels) - fov = self._calculate_field_of_view(item) - channel = self._calculate_channel(item) - z_level = self._calculate_z_level(item) - image_group_number = int(item / len(self.channels)) - frame_number = self._calculate_frame_number(image_group_number, fov, z_level) - timestamp, raw_image_data = self._get_raw_image_data(image_group_number, channel_offset) - image = Image(timestamp, frame_number, raw_image_data, fov, channel, z_level, self.height, self.width) - except (TypeError, ValueError): - return None - except KeyError: - raise IndexError("Invalid frame number.") - else: - return image - elif isinstance(item, slice): - return self._slice(item.start, item.stop, item.step) - raise IndexError - - def _slice(self, start, stop, step): - """ - Allows for iteration over a selection of the entire dataset. - - :type start: int - :type stop: int - :type step: int - :rtype: nd2reader.model.Image() or None - - """ - start = start if start is not None else 0 - step = step if step is not None else 1 - stop = stop if stop is not None else len(self) - # This weird thing with the step allows you to iterate backwards over the images - for i in range(start, stop)[::step]: - yield self[i] - - @property - def image_sets(self): - """ - Iterates over groups of related images. This is useful if your ND2 contains multiple fields of view. - A typical use case might be that you have, say, four areas of interest that you're monitoring, and every - minute you take a bright field and GFP image of each one. For each cycle, this method would produce four - ImageSet objects, each containing one bright field and one GFP image. - - :return: model.ImageSet() - - """ - for time_index in self.time_indexes: - image_set = ImageSet() - for fov in self.fields_of_view: - for channel_name in self.channels: - for z_level in self.z_levels: - image = self.get_image(time_index, fov, channel_name, z_level) - if image is not None: - image_set.add(image) - yield image_set - - @property - def height(self): - """ - :return: height of each image, in pixels - :rtype: int - - """ - return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiHeight')] - - @property - def width(self): - """ - :return: width of each image, in pixels - :rtype: int - - """ - return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiWidth')] - - 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. In general, you should avoid - using this method unless you're very familiar with the structure of ND2 files. - - :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 - - """ - 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, self._channel_offset[channel_name]) - image = Image(timestamp, frame_number, raw_image_data, field_of_view, channel_name, z_level, self.height, self.width) - except TypeError: - return None - else: - return image +from nd2reader.interface import Nd2 diff --git a/nd2reader/driver/__init__.py b/nd2reader/driver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nd2reader/driver/v2.py b/nd2reader/driver/v2.py new file mode 100644 index 0000000..e69de29 diff --git a/nd2reader/driver/v3.py b/nd2reader/driver/v3.py new file mode 100644 index 0000000..ea00527 --- /dev/null +++ b/nd2reader/driver/v3.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- + +import array +import numpy as np +import struct +import six +from nd2reader.model.image import Image + + +class V3Driver(object): + CHUNK_HEADER = 0xabeceda + + def __init__(self, metadata, label_map, file_handle): + self._metadata = metadata + self._label_map = label_map + self._file_handle = file_handle + + def _calculate_field_of_view(self, frame_number): + 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) + + def _calculate_channel(self, frame_number): + return self._metadata.channels[frame_number % len(self._metadata.channels)] + + 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))] + + def _calculate_image_group_number(self, time_index, 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 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) + + 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)) + + def get_image(self, index): + channel_offset = index % len(self._metadata.channels) + fov = 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) + timestamp, image = self._get_raw_image_data(image_group_number, channel_offset, self._metadata.height, self._metadata.width) + image.add_params(timestamp, frame_number, fov, channel, z_level) + return image + + @property + def _channel_offset(self): + """ + Image data is interleaved for each image set. That is, if there are four images in a set, the first image + will consist of pixels 1, 5, 9, etc, the second will be pixels 2, 6, 10, and so forth. + + :rtype: dict + + """ + channel_offset = {} + for n, channel in enumerate(self._metadata.channels): + channel_offset[channel] = n + return channel_offset + + 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. + :type image_group_number: int + :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 + + """ + chunk = self._label_map[six.b("ImageDataSeq|%d!" % image_group_number)] + data = self._read_chunk(chunk) + # All images in the same image group share the same timestamp! So if you have complicated image data, + # your timestamps may not be entirely accurate. Practically speaking though, they'll only be off by a few + # seconds unless you're doing something super weird. + timestamp = struct.unpack("d", data[:8])[0] + image_group_data = array.array("H", data) + image_data_start = 4 + channel_offset + # The images for the various channels are interleaved within the same array. For example, the second image + # of a four image group will be composed of bytes 2, 6, 10, etc. If you understand why someone would design + # a data structure that way, please send the author of this library a message. + image_data = np.reshape(image_group_data[image_data_start::len(self._metadata.channels)], (height, width)) + # Skip images that are all zeros! This is important, since NIS Elements creates blank "gap" images if you + # don't have the same number of images each cycle. We discovered this because we only took GFP images every + # other cycle to reduce phototoxicity, but NIS Elements still allocated memory as if we were going to take + # them every cycle. + if np.any(image_data): + return timestamp, Image(image_data) + return None + + def _read_chunk(self, chunk_location): + """ + Gets the data for a given chunk pointer + + :rtype: bytes + + """ + self._file_handle.seek(chunk_location) + # The chunk metadata is always 16 bytes long + chunk_metadata = self._file_handle.read(16) + header, relative_offset, data_length = struct.unpack("IIQ", chunk_metadata) + if header != V3Driver.CHUNK_HEADER: + raise ValueError("The ND2 file seems to be corrupted.") + # We start at the location of the chunk metadata, skip over the metadata, and then proceed to the + # start of the actual data field, which is at some arbitrary place after the metadata. + self._file_handle.seek(chunk_location + 16 + relative_offset) + return self._file_handle.read(data_length) diff --git a/nd2reader/exc.py b/nd2reader/exc.py new file mode 100644 index 0000000..eda1f2d --- /dev/null +++ b/nd2reader/exc.py @@ -0,0 +1,2 @@ +class InvalidVersionError(Exception): + pass diff --git a/nd2reader/interface.py b/nd2reader/interface.py new file mode 100644 index 0000000..a54b5dd --- /dev/null +++ b/nd2reader/interface.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- + +from nd2reader.model import ImageGroup +from nd2reader.parser import get_parser +from nd2reader.version import get_version +import warnings + + +class Nd2(object): + """ + Allows easy access to NIS Elements .nd2 image files. + + """ + def __init__(self, filename): + major_version, minor_version = get_version(filename) + parser = get_parser(filename, major_version, minor_version) + self._driver = parser.driver + self._metadata = parser.metadata + self._filename = filename + + def __repr__(self): + return "\n".join(["" % self._filename, + "Created: %s" % self.date, + "Image size: %sx%s (HxW)" % (self.height, self.width), + "Frames: %s" % len(self.frames), + "Channels: %s" % ", ".join(["'%s'" % str(channel) for channel in self.channels]), + "Fields of View: %s" % len(self.fields_of_view), + "Z-Levels: %s" % len(self.z_levels) + ]) + + def __len__(self): + """ + 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 + + """ + return self._driver.total_images_per_channel * len(self.channels) + + def __getitem__(self, item): + """ + Allows slicing ND2s. + + >>> nd2 = Nd2("my_images.nd2") + >>> image = nd2[16] # gets 17th frame + >>> for image in nd2[100:200]: # iterate over the 100th to 200th images + >>> do_something(image) + >>> for image in nd2[::-1]: # iterate backwards + >>> do_something(image) + >>> for image in nd2[37:422:17]: # do something super weird if you really want to + >>> do_something(image) + + :type item: int or slice + :rtype: nd2reader.model.Image() or generator + + """ + if isinstance(item, int): + return self._driver.get_image(item) + elif isinstance(item, slice): + return self._slice(item.start, item.stop, item.step) + raise IndexError + + def _slice(self, start, stop, step): + """ + Allows for iteration over a selection of the entire dataset. + + :type start: int + :type stop: int + :type step: int + :rtype: nd2reader.model.Image() + + """ + start = start if start is not None else 0 + step = step if step is not None else 1 + stop = stop if stop is not None else len(self) + # This weird thing with the step allows you to iterate backwards over the images + for i in range(start, stop)[::step]: + yield self[i] + + @property + def image_sets(self): + """ + Iterates over groups of related images. This is useful if your ND2 contains multiple fields of view. + A typical use case might be that you have, say, four areas of interest that you're monitoring, and every + minute you take a bright field and GFP image of each one. For each cycle, this method would produce four + ImageSet objects, each containing one bright field and one GFP image. + + :return: model.ImageSet() + + """ + warnings.warn("Nd2.image_sets will be removed from the nd2reader library in the near future.", DeprecationWarning) + + for frame in self.frames: + image_group = ImageGroup() + for fov in self.fields_of_view: + for channel_name in self.channels: + for z_level in self.z_levels: + image = self.get_image(frame, fov, channel_name, z_level) + if image is not None: + image_group.add(image) + yield image_group + + @property + def date(self): + return self._metadata.date + + @property + def z_levels(self): + return self._metadata.z_levels + + @property + def fields_of_view(self): + return self._metadata.fields_of_view + + @property + def channels(self): + return self._metadata.channels + + @property + def frames(self): + return self._metadata.frames + + @property + def height(self): + """ + :return: height of each image, in pixels + :rtype: int + + """ + return self._metadata.height + + @property + def width(self): + """ + :return: width of each image, in pixels + :rtype: int + + """ + return self._metadata.width + + 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. + + :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() + + """ + return self._driver.get_image_by_attributes(frame_number, field_of_view, channel_name, z_level) diff --git a/nd2reader/model/__init__.py b/nd2reader/model/__init__.py index df1ff5f..f7d3fa8 100644 --- a/nd2reader/model/__init__.py +++ b/nd2reader/model/__init__.py @@ -1,155 +1,2 @@ -# -*- coding: utf-8 -*- - -import collections -import numpy as np -import logging - -log = logging.getLogger(__name__) - - -class Image(object): - def __init__(self, timestamp, frame_number, raw_array, field_of_view, channel, z_level, height, width): - """ - A wrapper around the raw pixel data of an image. - - :param timestamp: The frame number relative to the . - :type timestamp: int - :param timestamp: The number of milliseconds after the beginning of the acquisition that this image was taken. - :type timestamp: int - :param raw_array: The raw sequence of bytes that represents the image. - :type raw_array: array.array() - :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: The name of the color of this image - :type channel: str - :param z_level: The label for the location in the Z-plane where this image was taken. - :type z_level: int - :param height: The height of the image in pixels. - :type height: int - :param width: The width of the image in pixels. - :type width: int - - """ - self._timestamp = timestamp - self._frame_number = int(frame_number) - self._raw_data = raw_array - self._field_of_view = field_of_view - self._channel = channel - self._z_level = z_level - self._height = height - self._width = width - self._data = None - - def __repr__(self): - return "\n".join(["", - "%sx%s (HxW)" % (self._height, self._width), - "Timestamp: %s" % self.timestamp, - "Frame: %s" % self._frame_number, - "Field of View: %s" % self.field_of_view, - "Channel: %s" % self.channel, - "Z-Level: %s" % self.z_level, - ]) - - @property - def data(self): - """ - The actual image data. - - :rtype np.array() - - """ - if self._data is None: - # The data is just a 1-dimensional array originally. - # We convert it to a 2D image here. - self._data = np.reshape(self._raw_data, (self._height, self._width)) - return self._data - - @property - def field_of_view(self): - """ - Which of the fixed locations this image was taken at. - - :rtype int: - - """ - return self._field_of_view - - @property - def timestamp(self): - """ - 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. - - :rtype float: - - """ - return self._timestamp / 1000.0 - - @property - def frame_number(self): - return self._frame_number - - @property - def channel(self): - """ - The name of the filter used to acquire this image. These are user-supplied in NIS Elements. - - :rtype str: - - """ - return self._channel - - @property - def z_level(self): - """ - The vertical offset of the image. These are simple integers starting from 0, where the 0 is the lowest - z-level and each subsequent level incremented by 1. - - For example, if you acquired images at -3 µm, 0 µm, and +3 µm, your z-levels would be: - - -3 µm: 0 - 0 µm: 1 - +3 µm: 2 - - :rtype int: - - """ - return self._z_level - - -class ImageSet(object): - """ - A group of images that were taken at roughly the same time. - - """ - def __init__(self): - self._images = collections.defaultdict(dict) - - def __len__(self): - """ The number of images in the image set. """ - return sum([len(channel) for channel in self._images.values()]) - - def __repr__(self): - return "\n".join(["", - "Image count: %s" % len(self)]) - - def get(self, channel, z_level=0): - """ - Retrieve an image with a given channel and z-level. For most users, z_level will always be 0. - - :type channel: str - :type z_level: int - - """ - return self._images.get(channel).get(z_level) - - def add(self, image): - """ - Stores an image. - - :type image: nd2reader.model.Image() - - """ - self._images[image.channel][image.z_level] = image +from nd2reader.model.image import Image +from nd2reader.model.group import ImageGroup diff --git a/nd2reader/model/group.py b/nd2reader/model/group.py new file mode 100644 index 0000000..8d6bf04 --- /dev/null +++ b/nd2reader/model/group.py @@ -0,0 +1,37 @@ +import collections + + +class ImageGroup(object): + """ + A group of images that were taken at roughly the same time and in the same field of view. + + """ + def __init__(self): + self._images = collections.defaultdict(dict) + + def __len__(self): + """ The number of images in the image set. """ + return sum([len(channel) for channel in self._images.values()]) + + def __repr__(self): + return "\n".join(["", + "Image count: %s" % len(self)]) + + def get(self, channel, z_level=0): + """ + Retrieve an image with a given channel and z-level. For most users, z_level will always be 0. + + :type channel: str + :type z_level: int + + """ + return self._images.get(channel).get(z_level) + + def add(self, image): + """ + Stores an image. + + :type image: nd2reader.model.Image() + + """ + self._images[image.channel][image.z_level] = image diff --git a/nd2reader/model/image.py b/nd2reader/model/image.py new file mode 100644 index 0000000..f51d586 --- /dev/null +++ b/nd2reader/model/image.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- + +import numpy as np + + +class Image(np.ndarray): + def __new__(cls, array): + return np.asarray(array).view(cls) + + def __init__(self, array): + self._timestamp = None + self._frame_number = None + self._field_of_view = None + self._channel = None + self._z_level = None + + 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 frame number relative to the . + :type timestamp: int + :param timestamp: The number of milliseconds after the beginning of the acquisition that this image was taken. + :type timestamp: 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: The name of the color of this image + :type channel: str + :param z_level: The label for the location in the Z-plane where this image was taken. + :type z_level: int + + """ + self._timestamp = timestamp + self._frame_number = int(frame_number) + self._field_of_view = field_of_view + self._channel = channel + self._z_level = z_level + + @property + def height(self): + return self.shape[1] + + @property + def width(self): + return self.shape[0] + + @property + def field_of_view(self): + """ + Which of the fixed locations this image was taken at. + + :rtype int: + + """ + return self._field_of_view + + @property + def timestamp(self): + """ + 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. + + :rtype float: + + """ + return self._timestamp / 1000.0 + + @property + def frame_number(self): + return self._frame_number + + @property + def channel(self): + """ + The name of the filter used to acquire this image. These are user-supplied in NIS Elements. + + :rtype str: + + """ + return self._channel + + @property + def z_level(self): + """ + The vertical offset of the image. These are simple integers starting from 0, where the 0 is the lowest + z-level and each subsequent level incremented by 1. + + For example, if you acquired images at -3 µm, 0 µm, and +3 µm, your z-levels would be: + + -3 µm: 0 + 0 µm: 1 + +3 µm: 2 + + :rtype int: + + """ + return self._z_level diff --git a/nd2reader/model/metadata.py b/nd2reader/model/metadata.py new file mode 100644 index 0000000..838dcd7 --- /dev/null +++ b/nd2reader/model/metadata.py @@ -0,0 +1,72 @@ +class Metadata(object): + """ A simple container for ND2 metadata. """ + def __init__(self, height, width, channels, date, fields_of_view, frames, z_levels): + self._height = height + self._width = width + self._channels = channels + self._date = date + self._fields_of_view = fields_of_view + self._frames = frames + self._z_levels = z_levels + + @property + def height(self): + return self._height + + @property + def width(self): + return self._width + + @property + def date(self): + """ + The date and time when acquisition began. + + :rtype: datetime.datetime() + + """ + return self._date + + @property + def channels(self): + """ + 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.) + + :rtype: list + + """ + return self._channels + + @property + def fields_of_view(self): + """ + The metadata contains information about fields of view, but it contains it even if some fields + of view were cropped. We can't find anything that states which fields of view are actually + 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 + + """ + return self._fields_of_view + + @property + def frames(self): + """ + The number of cycles. + + :rtype: list + + """ + return self._frames + + @property + def z_levels(self): + """ + The different levels in the Z-plane. Just a sequence from 0 to n. + + :rtype: list + + """ + return self._z_levels diff --git a/nd2reader/parser.py b/nd2reader/parser.py deleted file mode 100644 index 08d41ee..0000000 --- a/nd2reader/parser.py +++ /dev/null @@ -1,386 +0,0 @@ -# -*- coding: utf-8 -*- - -import array -from datetime import datetime -import numpy as np -import re -import struct -import six - - -class Nd2Parser(object): - """ - Reads .nd2 files, provides an interface to the metadata, and generates numpy arrays from the image data. - You should not ever need to instantiate this class manually unless you're a developer. - - """ - 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, filename): - self._absolute_start = None - self._filename = filename - self._fh = None - self._channels = None - self._channel_count = None - self._chunk_map_start_location = None - self._cursor_position = 0 - self._dimension_text = None - self._fields_of_view = None - self._label_map = {} - self.metadata = {} - self._read_map() - self._time_indexes = None - self._parse_metadata() - self._z_levels = None - - @property - def absolute_start(self): - """ - The date and time when acquisition began. - - :rtype: datetime.datetime() - - """ - if self._absolute_start is None: - for line in self.metadata[six.b('ImageTextInfo')][six.b('SLxImageTextInfo')].values(): - line = line.decode("utf8") - absolute_start_12 = None - absolute_start_24 = None - # ND2s seem to randomly switch between 12- and 24-hour representations. - try: - absolute_start_24 = datetime.strptime(line, "%m/%d/%Y %H:%M:%S") - except (TypeError, ValueError): - pass - try: - absolute_start_12 = datetime.strptime(line, "%m/%d/%Y %I:%M:%S %p") - except (TypeError, ValueError): - pass - if not absolute_start_12 and not absolute_start_24: - continue - return absolute_start_12 if absolute_start_12 else absolute_start_24 - raise ValueError("This ND2 has no recorded start time. This is probably a bug.") - return self._absolute_start - - @property - def channels(self): - """ - 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.) - - :rtype: list - - """ - if not self._channels: - self._channels = [] - metadata = self.metadata[six.b('ImageMetadataSeq')][six.b('SLxPictureMetadata')][six.b('sPicturePlanes')] - try: - validity = self.metadata[six.b('ImageMetadata')][six.b('SLxExperiment')][six.b('ppNextLevelEx')][six.b('')][0][six.b('ppNextLevelEx')][six.b('')][0][six.b('pItemValid')] - except KeyError: - # If none of the channels have been deleted, there is no validity list, so we just make one - validity = [True for _ in metadata] - # Channel information is contained in dictionaries with the keys a0, a1...an where the number - # indicates the order in which the channel is stored. So by sorting the dicts alphabetically - # we get the correct order. - for (label, chan), valid in zip(sorted(metadata[six.b('sPlaneNew')].items()), validity): - if not valid: - continue - self._channels.append(chan[six.b('sDescription')].decode("utf8")) - return self._channels - - @property - def fields_of_view(self): - """ - The metadata contains information about fields of view, but it contains it even if some fields - of view were cropped. We can't find anything that states which fields of view are actually - 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 - - """ - if self._fields_of_view is None: - self._fields_of_view = self._parse_dimension_text(r""".*?XY\((\d+)\).*?""") - return self._fields_of_view - - @property - def time_indexes(self): - """ - The number of cycles. - - :rtype: list - - """ - if self._time_indexes is None: - self._time_indexes = self._parse_dimension_text(r""".*?T'\((\d+)\).*?""") - return self._time_indexes - - @property - def z_levels(self): - """ - The different levels in the Z-plane. Just a sequence from 0 to n. - - :rtype: list - - """ - if self._z_levels is None: - self._z_levels = self._parse_dimension_text(r""".*?Z\((\d+)\).*?""") - return self._z_levels - - def _calculate_field_of_view(self, frame_number): - images_per_cycle = len(self.z_levels) * len(self.channels) - return int((frame_number - (frame_number % images_per_cycle)) / images_per_cycle) % len(self.fields_of_view) - - def _calculate_channel(self, frame_number): - return self.channels[frame_number % len(self.channels)] - - def _calculate_z_level(self, frame_number): - return self.z_levels[int(((frame_number - (frame_number % len(self.channels))) / len(self.channels)) % len(self.z_levels))] - - @property - def _file_handle(self): - if self._fh is None: - self._fh = open(self._filename, "rb") - return self._fh - - def _get_raw_image_data(self, image_group_number, channel_offset): - """ - 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. - :type image_group_number: int - :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 - - """ - chunk = self._label_map[six.b("ImageDataSeq|%d!" % image_group_number)] - data = self._read_chunk(chunk) - # All images in the same image group share the same timestamp! So if you have complicated image data, - # your timestamps may not be entirely accurate. Practically speaking though, they'll only be off by a few - # seconds unless you're doing something super weird. - timestamp = struct.unpack("d", data[:8])[0] - image_group_data = array.array("H", data) - image_data_start = 4 + channel_offset - # The images for the various channels are interleaved within the same array. For example, the second image - # of a four image group will be composed of bytes 2, 6, 10, etc. If you understand why someone would design - # a data structure that way, please send the author of this library a message. - image_data = image_group_data[image_data_start::len(self.channels)] - # Skip images that are all zeros! This is important, since NIS Elements creates blank "gap" images if you - # don't have the same number of images each cycle. We discovered this because we only took GFP images every - # other cycle to reduce phototoxicity, but NIS Elements still allocated memory as if we were going to take - # them every cycle. - if np.any(image_data): - return timestamp, image_data - return None - - @property - def _dimensions(self): - """ - While there are metadata values that represent a lot of what we want to capture, they seem to be unreliable. - 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 - - """ - if self._dimension_text is None: - for line in self.metadata[six.b('ImageTextInfo')][six.b('SLxImageTextInfo')].values(): - if six.b("Dimensions:") in line: - metadata = line - break - else: - raise ValueError("Could not parse metadata dimensions!") - for line in metadata.split(six.b("\r\n")): - if line.startswith(six.b("Dimensions:")): - self._dimension_text = line - break - else: - raise ValueError("Could not parse metadata dimensions!") - return self._dimension_text - - def _calculate_image_group_number(self, time_index, 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 fov: int - :type z_level: int - - :rtype: int - - """ - return time_index * len(self.fields_of_view) * len(self.z_levels) + (fov * len(self.z_levels) + z_level) - - def _calculate_frame_number(self, image_group_number, fov, z_level): - return (image_group_number - (fov * len(self.z_levels) + z_level)) / (len(self.fields_of_view) * len(self.z_levels)) - - @property - def _channel_offset(self): - """ - Image data is interleaved for each image set. That is, if there are four images in a set, the first image - will consist of pixels 1, 5, 9, etc, the second will be pixels 2, 6, 10, and so forth. - - :rtype: dict - - """ - channel_offset = {} - for n, channel in enumerate(self._channels): - channel_offset[channel] = n - return channel_offset - - def _parse_dimension_text(self, pattern): - try: - count = int(re.match(pattern, self._dimensions).group(1)) - except AttributeError: - return [0] - except TypeError: - match = re.match(pattern, self._dimensions.decode("utf8")) - if not match: - return [0] - return list(range(int(match.group(1)))) - else: - return list(range(count)) - - @property - def _total_images_per_channel(self): - """ - The total number of images per channel. Warning: this may be inaccurate as it includes "gap" images. - - :rtype: int - - """ - return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiSequenceCount')] - - def _parse_metadata(self): - """ - Reads all metadata. - - """ - for label in self._label_map.keys(): - if label.endswith(six.b("LV!")) or six.b("LV|") in label: - data = self._read_chunk(self._label_map[label]) - stop = label.index(six.b("LV")) - self.metadata[label[:stop]] = self._read_metadata(data, 1) - - def _read_map(self): - """ - Every label ends with an exclamation point, however, we can't directly search for those to find all the labels - as some of the bytes contain the value 33, which is the ASCII code for "!". So we iteratively find each label, - grab the subsequent data (always 16 bytes long), advance to the next label and repeat. - - """ - self._file_handle.seek(-8, 2) - chunk_map_start_location = struct.unpack("Q", self._file_handle.read(8))[0] - self._file_handle.seek(chunk_map_start_location) - raw_text = self._file_handle.read(-1) - label_start = raw_text.index(Nd2Parser.CHUNK_MAP_START) + 32 - - while True: - data_start = raw_text.index(six.b("!"), label_start) + 1 - key = raw_text[label_start: data_start] - location, length = struct.unpack("QQ", raw_text[data_start: data_start + 16]) - if key == Nd2Parser.CHUNK_MAP_END: - # We've reached the end of the chunk map - break - self._label_map[key] = location - label_start = data_start + 16 - - def _read_chunk(self, chunk_location): - """ - Gets the data for a given chunk pointer - - """ - self._file_handle.seek(chunk_location) - # The chunk metadata is always 16 bytes long - chunk_metadata = self._file_handle.read(16) - header, relative_offset, data_length = struct.unpack("IIQ", chunk_metadata) - if header != Nd2Parser.CHUNK_HEADER: - raise ValueError("The ND2 file seems to be corrupted.") - # We start at the location of the chunk metadata, skip over the metadata, and then proceed to the - # start of the actual data field, which is at some arbitrary place after the metadata. - self._file_handle.seek(chunk_location + 16 + relative_offset) - return self._file_handle.read(data_length) - - def _parse_unsigned_char(self, data): - return struct.unpack("B", data.read(1))[0] - - def _parse_unsigned_int(self, data): - return struct.unpack("I", data.read(4))[0] - - def _parse_unsigned_long(self, data): - return struct.unpack("Q", data.read(8))[0] - - def _parse_double(self, data): - return struct.unpack("d", data.read(8))[0] - - def _parse_string(self, data): - value = data.read(2) - while not value.endswith(six.b("\x00\x00")): - # the string ends at the first instance of \x00\x00 - value += data.read(2) - return value.decode("utf16")[:-1].encode("utf8") - - def _parse_char_array(self, data): - array_length = struct.unpack("Q", data.read(8))[0] - return array.array("B", data.read(array_length)) - - def _parse_metadata_item(self, data): - """ - Reads hierarchical data, analogous to a Python dict. - - """ - new_count, length = struct.unpack("\d)\.(?P\d)$""", data) + if match: + # We haven't seen a lot of ND2s but the ones we have seen conform to this + return int(match.group('major')), int(match.group('minor')) + raise InvalidVersionError("The version of the ND2 you specified is not supported.") diff --git a/tests/__init__.py b/tests/__init__.py index 0a3def9..00e90ab 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,183 +1,183 @@ -from nd2reader.parser import Nd2Parser -import unittest - - -class MockNd2Parser(object): - def __init__(self, channels, fields_of_view, z_levels): - self.channels = channels - self.fields_of_view = fields_of_view - self.z_levels = z_levels - - -class TestNd2Parser(unittest.TestCase): - def test_calculate_field_of_view_simple(self): - """ With a single field of view, the field of view should always be the same number (0). """ - nd2 = MockNd2Parser([''], [0], [0]) - for frame_number in range(1000): - result = Nd2Parser._calculate_field_of_view(nd2, frame_number) - self.assertEqual(result, 0) - - def test_calculate_field_of_view_two_channels(self): - nd2 = MockNd2Parser(['', 'GFP'], [0], [0]) - for frame_number in range(1000): - result = Nd2Parser._calculate_field_of_view(nd2, frame_number) - self.assertEqual(result, 0) - - def test_calculate_field_of_view_three_channels(self): - nd2 = MockNd2Parser(['', 'GFP', 'dsRed'], [0], [0]) - for frame_number in range(1000): - result = Nd2Parser._calculate_field_of_view(nd2, frame_number) - self.assertEqual(result, 0) - - def test_calculate_field_of_view_two_fovs(self): - nd2 = MockNd2Parser([''], [0, 1], [0]) - for frame_number in range(1000): - result = Nd2Parser._calculate_field_of_view(nd2, frame_number) - self.assertEqual(result, frame_number % 2) - - def test_calculate_field_of_view_two_fovs_two_zlevels(self): - nd2 = MockNd2Parser([''], [0, 1], [0, 1]) - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 0), 0) - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 1), 0) - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 2), 1) - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 3), 1) - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 4), 0) - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 5), 0) - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 6), 1) - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 7), 1) - - def test_calculate_field_of_view_two_everything(self): - nd2 = MockNd2Parser(['', 'GFP'], [0, 1], [0, 1]) - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 0), 0) - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 1), 0) - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 2), 0) - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 3), 0) - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 4), 1) - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 5), 1) - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 6), 1) - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 7), 1) - - def test_calculate_field_of_view_7c2f2z(self): - nd2 = MockNd2Parser(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], [0, 1], [0, 1]) - for i in range(14): - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 0) - for i in range(14, 28): - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 1) - for i in range(28, 42): - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 0) - for i in range(42, 56): - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 1) - for i in range(56, 70): - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 0) - for i in range(70, 84): - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 1) - - def test_calculate_field_of_view_2c3f5z(self): - """ All prime numbers to elucidate any errors that won't show up when numbers are multiples of each other """ - nd2 = MockNd2Parser(['', 'GFP'], [0, 1, 2], [0, 1, 2, 3, 4]) - for i in range(10): - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 0) - for i in range(10, 20): - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 1) - for i in range(20, 30): - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 2) - for i in range(30, 40): - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 0) - for i in range(40, 50): - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 1) - for i in range(50, 60): - self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 2) - - def test_calculate_channel_simple(self): - nd2 = MockNd2Parser(['GFP'], [0], [0]) - for i in range(1000): - self.assertEqual(Nd2Parser._calculate_channel(nd2, i), 'GFP') - - def test_calculate_channel(self): - nd2 = MockNd2Parser(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], [0], [0]) - for i in range(1000): - for n, channel in enumerate(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], start=i*7): - self.assertEqual(Nd2Parser._calculate_channel(nd2, n), channel) - - def test_calculate_channel_7c2fov1z(self): - nd2 = MockNd2Parser(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], [0, 1], [0]) - for i in range(1000): - for n, channel in enumerate(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], start=i*7): - self.assertEqual(Nd2Parser._calculate_channel(nd2, n), channel) - - def test_calculate_channel_ludicrous_values(self): - nd2 = MockNd2Parser(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], list(range(31)), list(range(17))) - for i in range(10000): - for n, channel in enumerate(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], start=i*7): - self.assertEqual(Nd2Parser._calculate_channel(nd2, n), channel) - - def test_calculate_z_level(self): - nd2 = MockNd2Parser([''], [0], [0]) - for frame_number in range(1000): - result = Nd2Parser._calculate_z_level(nd2, frame_number) - self.assertEqual(result, 0) - - def test_calculate_z_level_1c1f2z(self): - nd2 = MockNd2Parser([''], [0], [0, 1]) - for frame_number in range(1000): - result = Nd2Parser._calculate_z_level(nd2, frame_number) - self.assertEqual(result, frame_number % 2) - - def test_calculate_z_level_31c17f1z(self): - nd2 = MockNd2Parser(list(range(31)), list(range(17)), [0]) - for frame_number in range(1000): - result = Nd2Parser._calculate_z_level(nd2, frame_number) - self.assertEqual(result, 0) - - def test_calculate_z_level_2c1f2z(self): - nd2 = MockNd2Parser(['', 'GFP'], [0], [0, 1]) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 0), 0) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 1), 0) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 2), 1) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 3), 1) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 4), 0) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 5), 0) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 6), 1) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 7), 1) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 8), 0) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 9), 0) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 10), 1) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 11), 1) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 12), 0) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 13), 0) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 14), 1) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 15), 1) - - def test_calculate_z_level_2c3f5z(self): - nd2 = MockNd2Parser(['', 'GFP'], [0, 1, 2], [0, 1, 2, 3, 4]) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 0), 0) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 1), 0) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 2), 1) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 3), 1) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 4), 2) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 5), 2) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 6), 3) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 7), 3) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 8), 4) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 9), 4) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 10), 0) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 11), 0) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 12), 1) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 13), 1) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 14), 2) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 15), 2) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 16), 3) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 17), 3) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 18), 4) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 19), 4) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 20), 0) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 21), 0) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 22), 1) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 23), 1) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 24), 2) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 25), 2) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 26), 3) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 27), 3) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 28), 4) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 29), 4) - self.assertEqual(Nd2Parser._calculate_z_level(nd2, 30), 0) \ No newline at end of file +# from nd2reader.parser import Nd2Parser +# import unittest +# +# +# class MockNd2Parser(object): +# def __init__(self, channels, fields_of_view, z_levels): +# self.channels = channels +# self.fields_of_view = fields_of_view +# self.z_levels = z_levels +# +# +# class TestNd2Parser(unittest.TestCase): +# def test_calculate_field_of_view_simple(self): +# """ With a single field of view, the field of view should always be the same number (0). """ +# nd2 = MockNd2Parser([''], [0], [0]) +# for frame_number in range(1000): +# result = Nd2Parser._calculate_field_of_view(nd2, frame_number) +# self.assertEqual(result, 0) +# +# def test_calculate_field_of_view_two_channels(self): +# nd2 = MockNd2Parser(['', 'GFP'], [0], [0]) +# for frame_number in range(1000): +# result = Nd2Parser._calculate_field_of_view(nd2, frame_number) +# self.assertEqual(result, 0) +# +# def test_calculate_field_of_view_three_channels(self): +# nd2 = MockNd2Parser(['', 'GFP', 'dsRed'], [0], [0]) +# for frame_number in range(1000): +# result = Nd2Parser._calculate_field_of_view(nd2, frame_number) +# self.assertEqual(result, 0) +# +# def test_calculate_field_of_view_two_fovs(self): +# nd2 = MockNd2Parser([''], [0, 1], [0]) +# for frame_number in range(1000): +# result = Nd2Parser._calculate_field_of_view(nd2, frame_number) +# self.assertEqual(result, frame_number % 2) +# +# def test_calculate_field_of_view_two_fovs_two_zlevels(self): +# nd2 = MockNd2Parser([''], [0, 1], [0, 1]) +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 0), 0) +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 1), 0) +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 2), 1) +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 3), 1) +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 4), 0) +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 5), 0) +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 6), 1) +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 7), 1) +# +# def test_calculate_field_of_view_two_everything(self): +# nd2 = MockNd2Parser(['', 'GFP'], [0, 1], [0, 1]) +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 0), 0) +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 1), 0) +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 2), 0) +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 3), 0) +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 4), 1) +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 5), 1) +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 6), 1) +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 7), 1) +# +# def test_calculate_field_of_view_7c2f2z(self): +# nd2 = MockNd2Parser(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], [0, 1], [0, 1]) +# for i in range(14): +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 0) +# for i in range(14, 28): +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 1) +# for i in range(28, 42): +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 0) +# for i in range(42, 56): +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 1) +# for i in range(56, 70): +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 0) +# for i in range(70, 84): +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 1) +# +# def test_calculate_field_of_view_2c3f5z(self): +# """ All prime numbers to elucidate any errors that won't show up when numbers are multiples of each other """ +# nd2 = MockNd2Parser(['', 'GFP'], [0, 1, 2], [0, 1, 2, 3, 4]) +# for i in range(10): +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 0) +# for i in range(10, 20): +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 1) +# for i in range(20, 30): +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 2) +# for i in range(30, 40): +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 0) +# for i in range(40, 50): +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 1) +# for i in range(50, 60): +# self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 2) +# +# def test_calculate_channel_simple(self): +# nd2 = MockNd2Parser(['GFP'], [0], [0]) +# for i in range(1000): +# self.assertEqual(Nd2Parser._calculate_channel(nd2, i), 'GFP') +# +# def test_calculate_channel(self): +# nd2 = MockNd2Parser(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], [0], [0]) +# for i in range(1000): +# for n, channel in enumerate(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], start=i*7): +# self.assertEqual(Nd2Parser._calculate_channel(nd2, n), channel) +# +# def test_calculate_channel_7c2fov1z(self): +# nd2 = MockNd2Parser(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], [0, 1], [0]) +# for i in range(1000): +# for n, channel in enumerate(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], start=i*7): +# self.assertEqual(Nd2Parser._calculate_channel(nd2, n), channel) +# +# def test_calculate_channel_ludicrous_values(self): +# nd2 = MockNd2Parser(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], list(range(31)), list(range(17))) +# for i in range(10000): +# for n, channel in enumerate(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], start=i*7): +# self.assertEqual(Nd2Parser._calculate_channel(nd2, n), channel) +# +# def test_calculate_z_level(self): +# nd2 = MockNd2Parser([''], [0], [0]) +# for frame_number in range(1000): +# result = Nd2Parser._calculate_z_level(nd2, frame_number) +# self.assertEqual(result, 0) +# +# def test_calculate_z_level_1c1f2z(self): +# nd2 = MockNd2Parser([''], [0], [0, 1]) +# for frame_number in range(1000): +# result = Nd2Parser._calculate_z_level(nd2, frame_number) +# self.assertEqual(result, frame_number % 2) +# +# def test_calculate_z_level_31c17f1z(self): +# nd2 = MockNd2Parser(list(range(31)), list(range(17)), [0]) +# for frame_number in range(1000): +# result = Nd2Parser._calculate_z_level(nd2, frame_number) +# self.assertEqual(result, 0) +# +# def test_calculate_z_level_2c1f2z(self): +# nd2 = MockNd2Parser(['', 'GFP'], [0], [0, 1]) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 0), 0) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 1), 0) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 2), 1) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 3), 1) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 4), 0) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 5), 0) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 6), 1) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 7), 1) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 8), 0) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 9), 0) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 10), 1) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 11), 1) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 12), 0) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 13), 0) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 14), 1) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 15), 1) +# +# def test_calculate_z_level_2c3f5z(self): +# nd2 = MockNd2Parser(['', 'GFP'], [0, 1, 2], [0, 1, 2, 3, 4]) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 0), 0) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 1), 0) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 2), 1) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 3), 1) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 4), 2) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 5), 2) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 6), 3) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 7), 3) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 8), 4) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 9), 4) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 10), 0) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 11), 0) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 12), 1) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 13), 1) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 14), 2) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 15), 2) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 16), 3) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 17), 3) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 18), 4) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 19), 4) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 20), 0) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 21), 0) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 22), 1) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 23), 1) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 24), 2) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 25), 2) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 26), 3) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 27), 3) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 28), 4) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 29), 4) +# self.assertEqual(Nd2Parser._calculate_z_level(nd2, 30), 0) \ No newline at end of file diff --git a/tests/driver/__init__.py b/tests/driver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/driver/driver.py b/tests/driver/driver.py new file mode 100644 index 0000000..35752e4 --- /dev/null +++ b/tests/driver/driver.py @@ -0,0 +1,7 @@ +import unittest +from nd2reader.driver import get_driver + + +class TestDriver(unittest.TestCase): + def test_get_driver(self): + pass \ No newline at end of file diff --git a/tests/driver/version.py b/tests/driver/version.py new file mode 100644 index 0000000..9add147 --- /dev/null +++ b/tests/driver/version.py @@ -0,0 +1,16 @@ +import unittest +from nd2reader.driver.version import parse_version + + +class VersionTests(unittest.TestCase): + def test_parse_version_2(self): + data = 'ND2 FILE SIGNATURE CHUNK NAME01!Ver2.2' + actual = parse_version(data) + expected = (2, 2) + self.assertTupleEqual(actual, expected) + + def test_parse_version_3(self): + data = 'ND2 FILE SIGNATURE CHUNK NAME01!Ver3.0' + actual = parse_version(data) + expected = (3, 0) + self.assertTupleEqual(actual, expected)