Browse Source

Merge pull request #50 from jimrybarski/39-feature-getitem

39 feature getitem
master
Jim Rybarski 10 years ago
parent
commit
3e0d788e7c
3 changed files with 94 additions and 96 deletions
  1. +48
    -39
      nd2reader/__init__.py
  2. +45
    -56
      nd2reader/parser.py
  3. +1
    -1
      setup.py

+ 48
- 39
nd2reader/__init__.py View File

@ -18,10 +18,10 @@ class Nd2(Nd2Parser):
return "\n".join(["<ND2 %s>" % self._filename, return "\n".join(["<ND2 %s>" % self._filename,
"Created: %s" % self._absolute_start.strftime("%Y-%m-%d %H:%M:%S"), "Created: %s" % self._absolute_start.strftime("%Y-%m-%d %H:%M:%S"),
"Image size: %sx%s (HxW)" % (self.height, self.width), "Image size: %sx%s (HxW)" % (self.height, self.width),
"Image cycles: %s" % self._time_index_count,
"Channels: %s" % ", ".join(["'%s'" % str(channel) for channel in self._channels]),
"Fields of View: %s" % self._field_of_view_count,
"Z-Levels: %s" % self._z_level_count
"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): def __len__(self):
@ -34,6 +34,41 @@ class Nd2(Nd2Parser):
""" """
return self._image_count * self._channel_count return self._image_count * self._channel_count
def __getitem__(self, frame_number):
if isinstance(frame_number, int):
try:
channel_offset = frame_number % len(self.channels)
fov = self._calculate_field_of_view(frame_number)
channel = self._calculate_channel(frame_number)
z_level = self._calculate_z_level(frame_number)
timestamp, raw_image_data = self._get_raw_image_data(frame_number, channel_offset)
image = Image(timestamp, raw_image_data, fov, channel, z_level, self.height, self.width)
except (TypeError, ValueError):
return None
else:
return image
@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 @property
def height(self): def height(self):
""" """
@ -52,41 +87,15 @@ class Nd2(Nd2Parser):
""" """
return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiWidth')] return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiWidth')]
def __iter__(self):
"""
Iterates over every image, in the order they were taken.
:return: model.Image()
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)
"""
for i in range(self._image_count):
for fov in range(self._field_of_view_count):
for z_level in range(self._z_level_count):
for channel_name in self._channels:
image = self.get_image(i, fov, channel_name, z_level)
if image is not None:
yield image
def _calculate_channel(self, frame_number):
return self._channels[frame_number % len(self.channels)]
@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 range(self._time_index_count):
image_set = ImageSet()
for fov in range(self._field_of_view_count):
for channel_name in self._channels:
for z_level in range(self._z_level_count):
image = self.get_image(time_index, fov, channel_name, z_level)
if image is not None:
image_set.add(image)
yield image_set
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))]
def get_image(self, time_index, field_of_view, channel_name, z_level): def get_image(self, time_index, field_of_view, channel_name, z_level):
""" """
@ -105,9 +114,9 @@ class Nd2(Nd2Parser):
:rtype: nd2reader.model.Image() or None :rtype: nd2reader.model.Image() or None
""" """
image_set_number = self._calculate_image_group_number(time_index, field_of_view, z_level)
image_group_number = self._calculate_image_group_number(time_index, field_of_view, z_level)
try: try:
timestamp, raw_image_data = self._get_raw_image_data(image_set_number, self._channel_offset[channel_name])
timestamp, raw_image_data = self._get_raw_image_data(image_group_number, self._channel_offset[channel_name])
image = Image(timestamp, raw_image_data, field_of_view, channel_name, z_level, self.height, self.width) image = Image(timestamp, raw_image_data, field_of_view, channel_name, z_level, self.height, self.width)
except TypeError: except TypeError:
return None return None


+ 45
- 56
nd2reader/parser.py View File

@ -21,13 +21,18 @@ class Nd2Parser(object):
def __init__(self, filename): def __init__(self, filename):
self._filename = filename self._filename = filename
self._fh = None self._fh = None
self._channels = None
self._channel_count = None
self._chunk_map_start_location = None self._chunk_map_start_location = None
self._cursor_position = 0 self._cursor_position = 0
self._dimension_text = None self._dimension_text = None
self._fields_of_view = None
self._label_map = {} self._label_map = {}
self.metadata = {} self.metadata = {}
self._read_map() self._read_map()
self._time_indexes = None
self._parse_metadata() self._parse_metadata()
self._z_levels = None
@property @property
def _file_handle(self): def _file_handle(self):
@ -58,7 +63,7 @@ class Nd2Parser(object):
# The images for the various channels are interleaved within the same array. For example, the second image # 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 # 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. # a data structure that way, please send the author of this library a message.
image_data = image_group_data[image_data_start::self._channel_count]
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 # 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 # 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 # other cycle to reduce phototoxicity, but NIS Elements still allocated memory as if we were going to take
@ -90,12 +95,10 @@ class Nd2Parser(object):
break break
else: else:
raise ValueError("Could not parse metadata dimensions!") raise ValueError("Could not parse metadata dimensions!")
if six.PY3:
return self._dimension_text.decode("utf8")
return self._dimension_text return self._dimension_text
@property @property
def _channels(self):
def channels(self):
""" """
These are labels created by the NIS Elements user. Typically they may a short description of the filter cube 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.) used (e.g. "bright field", "GFP", etc.)
@ -103,19 +106,22 @@ class Nd2Parser(object):
:rtype: str :rtype: str
""" """
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
yield chan[six.b('sDescription')].decode("utf8")
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
def _calculate_image_group_number(self, time_index, fov, z_level): def _calculate_image_group_number(self, time_index, fov, z_level):
""" """
@ -128,7 +134,7 @@ class Nd2Parser(object):
:rtype: int :rtype: int
""" """
return time_index * self._field_of_view_count * self._z_level_count + (fov * self._z_level_count + z_level)
return time_index * len(self.fields_of_view) * len(self.z_levels) + (fov * len(self.z_levels) + z_level)
@property @property
def _channel_offset(self): def _channel_offset(self):
@ -170,24 +176,19 @@ class Nd2Parser(object):
return absolute_start_12 if absolute_start_12 else absolute_start_24 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.") raise ValueError("This ND2 has no recorded start time. This is probably a bug.")
@property
def _channel_count(self):
"""
The number of different channels used, including bright field.
:rtype: int
"""
pattern = r""".*?λ\((\d+)\).*?"""
def _parse_dimension_text(self, pattern):
try: try:
count = int(re.match(pattern, self._dimensions).group(1)) count = int(re.match(pattern, self._dimensions).group(1))
except AttributeError as e:
return 1
except AttributeError:
return [0]
except TypeError:
count = int(re.match(pattern, self._dimensions.decode("utf8")).group(1))
return list(range(count))
else: else:
return count
return list(range(count))
@property @property
def _field_of_view_count(self):
def fields_of_view(self):
""" """
The metadata contains information about fields of view, but it contains it even if some fields 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 of view were cropped. We can't find anything that states which fields of view are actually
@ -197,45 +198,33 @@ class Nd2Parser(object):
:rtype: int :rtype: int
""" """
pattern = r""".*?XY\((\d+)\).*?"""
try:
count = int(re.match(pattern, self._dimensions).group(1))
except AttributeError:
return 1
else:
return count
if self._fields_of_view is None:
self._fields_of_view = self._parse_dimension_text(r""".*?XY\((\d+)\).*?""")
return self._fields_of_view
@property @property
def _time_index_count(self):
def time_indexes(self):
""" """
The number of cycles. The number of cycles.
:rtype: int :rtype: int
""" """
pattern = r""".*?T'\((\d+)\).*?"""
try:
count = int(re.match(pattern, self._dimensions).group(1))
except AttributeError:
return 1
else:
return count
if self._time_indexes is None:
self._time_indexes = self._parse_dimension_text(r""".*?T'\((\d+)\).*?""")
return self._time_indexes
@property @property
def _z_level_count(self):
def z_levels(self):
""" """
The number of different levels in the Z-plane.
The different levels in the Z-plane. Just a sequence from 0 to n.
:rtype: int :rtype: int
""" """
pattern = r""".*?Z\((\d+)\).*?"""
try:
count = int(re.match(pattern, self._dimensions).group(1))
except AttributeError:
return 1
else:
return count
if self._z_levels is None:
self._z_levels = self._parse_dimension_text(r""".*?Z\((\d+)\).*?""")
return self._z_levels
@property @property
def _image_count(self): def _image_count(self):
@ -376,4 +365,4 @@ class Nd2Parser(object):
# We've encountered this key before so we're guaranteed to be dealing with a list. Thus we append # We've encountered this key before so we're guaranteed to be dealing with a list. Thus we append
# the value to the already-existing list. # the value to the already-existing list.
metadata[name].append(value) metadata[name].append(value)
return metadata
return metadata

+ 1
- 1
setup.py View File

@ -1,6 +1,6 @@
from distutils.core import setup from distutils.core import setup
VERSION = "1.0.1"
VERSION = "1.1.0"
setup( setup(
name="nd2reader", name="nd2reader",


Loading…
Cancel
Save