diff --git a/nd2reader/driver/v3.py b/nd2reader/driver/v3.py index 0dca2aa..c5a095b 100644 --- a/nd2reader/driver/v3.py +++ b/nd2reader/driver/v3.py @@ -6,6 +6,7 @@ import numpy as np import re import struct import six +from nd2reader.model.image import Image class Nd2Parser(object): @@ -29,12 +30,22 @@ class Nd2Parser(object): self._dimension_text = None self._fields_of_view = None self._label_map = {} - self.metadata = {} + self._metadata = {} self._read_map() self._time_indexes = None self._parse_metadata() self._z_levels = None + 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) + @property def absolute_start(self): """ @@ -144,7 +155,7 @@ class Nd2Parser(object): self._fh = open(self._filename, "rb") return self._fh - def _get_raw_image_data(self, image_group_number, 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. @@ -167,13 +178,13 @@ class Nd2Parser(object): # 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)] + image_data = np.reshape(image_group_data[image_data_start::len(self.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_data + return timestamp, Image(image_data) return None @property diff --git a/nd2reader/interface.py b/nd2reader/interface.py index 0ff6c90..b02c4c0 100644 --- a/nd2reader/interface.py +++ b/nd2reader/interface.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -from nd2reader.model import Image, ImageGroup +from nd2reader.model import ImageGroup from nd2reader.driver import get_driver from nd2reader.driver.version import get_version -import six +import warnings class Nd2(object): @@ -12,13 +12,14 @@ class Nd2(object): """ def __init__(self, filename): + self._filename = filename version = get_version(filename) self._driver = get_driver(filename, version) self._metadata = self._driver.get_metadata() def __repr__(self): - return "\n".join(["" % self._driver._filename, - "Created: %s" % self._driver.absolute_start, + 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]), @@ -43,32 +44,18 @@ class Nd2(object): >>> 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) + >>> do_something(image) >>> for image in nd2[::-1]: # iterate backwards - >>> do_something(image.data) + >>> do_something(image) >>> for image in nd2[37:422:17]: # do something super weird if you really want to - >>> do_something(image.data) + >>> do_something(image) :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 + return self._driver.get_image(item) elif isinstance(item, slice): return self._slice(item.start, item.stop, item.step) raise IndexError @@ -80,7 +67,7 @@ class Nd2(object): :type start: int :type stop: int :type step: int - :rtype: nd2reader.model.Image() or None + :rtype: nd2reader.model.Image() """ start = start if start is not None else 0 @@ -101,15 +88,37 @@ class Nd2(object): :return: model.ImageSet() """ - for time_index in self.time_indexes: - image_set = ImageGroup() + 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(time_index, fov, channel_name, z_level) + image = self.get_image(frame, fov, channel_name, z_level) if image is not None: - image_set.add(image) - yield image_set + 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): @@ -118,7 +127,7 @@ class Nd2(object): :rtype: int """ - return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiHeight')] + return self._metadata.height @property def width(self): @@ -127,12 +136,11 @@ class Nd2(object): :rtype: int """ - return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiWidth')] + 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. In general, you should avoid - using this method unless you're very familiar with the structure of ND2 files. + 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. @@ -141,14 +149,8 @@ class Nd2(object): :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 + + :rtype: nd2reader.model.Image() """ - 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 + return self._driver.get_image_by_attributes(frame_number, field_of_view, channel_name, z_level) diff --git a/nd2reader/model/image.py b/nd2reader/model/image.py index e81c8d1..31ad0c6 100644 --- a/nd2reader/model/image.py +++ b/nd2reader/model/image.py @@ -3,8 +3,11 @@ import numpy as np -class Image(object): - def __init__(self, timestamp, frame_number, raw_array, field_of_view, channel, z_level, height, width): +class Image(np.ndarray): + def __new__(cls, array): + return np.asarray(array).view(cls) + + def add_params(self, timestamp, frame_number, field_of_view, channel, z_level): """ A wrapper around the raw pixel data of an image. @@ -12,8 +15,6 @@ class Image(object): :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 @@ -28,17 +29,13 @@ class Image(object): """ 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), + "%sx%s (HxW)" % (self.height, self.width), "Timestamp: %s" % self.timestamp, "Frame: %s" % self._frame_number, "Field of View: %s" % self.field_of_view, @@ -46,20 +43,6 @@ class Image(object): "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): """