Browse Source

#42 refactored and tidied up a bit, created test files

zolfa-add_slices_loading
jim 9 years ago
parent
commit
ae09b688b2
4 changed files with 103 additions and 95 deletions
  1. +2
    -2
      nd2reader/__init__.py
  2. +96
    -93
      nd2reader/parser.py
  3. +5
    -0
      tests.py
  4. +0
    -0
      tests/__init__.py

+ 2
- 2
nd2reader/__init__.py View File

@ -16,7 +16,7 @@ class Nd2(Nd2Parser):
def __repr__(self):
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 cycles: %s" % len(self.time_indexes),
"Channels: %s" % ", ".join(["'%s'" % str(channel) for channel in self.channels]),
@ -32,7 +32,7 @@ class Nd2(Nd2Parser):
:rtype: int
"""
return self._image_count * self._channel_count
return self._total_images_per_channel * self._channel_count
def __getitem__(self, item):
"""


+ 96
- 93
nd2reader/parser.py View File

@ -19,6 +19,7 @@ class Nd2Parser(object):
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
@ -34,6 +35,99 @@ class Nd2Parser(object):
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: str
"""
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: int
"""
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: int
"""
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: int
"""
if self._z_levels is None:
self._z_levels = self._parse_dimension_text(r""".*?Z\((\d+)\).*?""")
return self._z_levels
@property
def _file_handle(self):
if self._fh is None:
@ -97,32 +191,6 @@ class Nd2Parser(object):
raise ValueError("Could not parse metadata dimensions!")
return self._dimension_text
@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: str
"""
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):
"""
Images are grouped together if they share the same time index, field of view, and z-level.
@ -150,32 +218,6 @@ class Nd2Parser(object):
channel_offset[channel] = n
return channel_offset
@property
def _absolute_start(self):
"""
The date and time when acquisition began.
:rtype: datetime.datetime()
"""
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.")
def _parse_dimension_text(self, pattern):
try:
count = int(re.match(pattern, self._dimensions).group(1))
@ -188,48 +230,9 @@ class Nd2Parser(object):
return list(range(count))
@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: int
"""
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: int
"""
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: int
"""
if self._z_levels is None:
self._z_levels = self._parse_dimension_text(r""".*?Z\((\d+)\).*?""")
return self._z_levels
@property
def _image_count(self):
def _total_images_per_channel(self):
"""
The total number of images in the ND2. Warning: this may be inaccurate as it includes "gap" images.
The total number of images per channel. Warning: this may be inaccurate as it includes "gap" images.
:rtype: int


+ 5
- 0
tests.py View File

@ -0,0 +1,5 @@
import unittest
loader = unittest.TestLoader()
tests = loader.discover('tests', pattern='*.py', top_level_dir='.')
testRunner = unittest.TextTestRunner()
testRunner.run(tests)

+ 0
- 0
tests/__init__.py View File


Loading…
Cancel
Save