Browse Source

Merge pull request #53 from jimrybarski/42-feature-unittests

42 feature unittests
zolfa-add_slices_loading
Jim Rybarski 10 years ago
parent
commit
9eb8a4b762
6 changed files with 301 additions and 109 deletions
  1. +1
    -4
      Dockerfile
  2. +4
    -0
      Makefile
  3. +2
    -12
      nd2reader/__init__.py
  4. +106
    -93
      nd2reader/parser.py
  5. +5
    -0
      tests.py
  6. +183
    -0
      tests/__init__.py

+ 1
- 4
Dockerfile View File

@ -1,4 +1,4 @@
FROM ubuntu
FROM ubuntu:15.04
MAINTAINER Jim Rybarski <jim@rybarski.com> MAINTAINER Jim Rybarski <jim@rybarski.com>
RUN mkdir -p /var/nds2 RUN mkdir -p /var/nds2
@ -15,6 +15,3 @@ COPY . /opt/nd2reader
WORKDIR /opt/nd2reader WORKDIR /opt/nd2reader
RUN python setup.py install RUN python setup.py install
RUN python3 setup.py install RUN python3 setup.py install
WORKDIR /var/nd2s
CMD /usr/bin/python3.4

+ 4
- 0
Makefile View File

@ -8,3 +8,7 @@ py2shell:
py3shell: py3shell:
docker run --rm -v ~/Documents/nd2s:/var/nd2s -it jimrybarski/nd2reader python3.4 docker run --rm -v ~/Documents/nd2s:/var/nd2s -it jimrybarski/nd2reader python3.4
test: build
docker run --rm -it jimrybarski/nd2reader python3.4 /opt/nd2reader/tests.py

+ 2
- 12
nd2reader/__init__.py View File

@ -16,7 +16,7 @@ class Nd2(Nd2Parser):
def __repr__(self): def __repr__(self):
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" % len(self.time_indexes), "Image cycles: %s" % len(self.time_indexes),
"Channels: %s" % ", ".join(["'%s'" % str(channel) for channel in self.channels]), "Channels: %s" % ", ".join(["'%s'" % str(channel) for channel in self.channels]),
@ -32,7 +32,7 @@ class Nd2(Nd2Parser):
:rtype: int :rtype: int
""" """
return self._image_count * self._channel_count
return self._total_images_per_channel * self._channel_count
def __getitem__(self, item): def __getitem__(self, item):
""" """
@ -123,16 +123,6 @@ 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 _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))]
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):
""" """
Returns an Image if data exists for the given parameters, otherwise returns None. In general, you should avoid Returns an Image if data exists for the given parameters, otherwise returns None. In general, you should avoid


+ 106
- 93
nd2reader/parser.py View File

@ -19,6 +19,7 @@ class Nd2Parser(object):
CHUNK_MAP_END = six.b("ND2 CHUNK MAP SIGNATURE 0000001!") CHUNK_MAP_END = six.b("ND2 CHUNK MAP SIGNATURE 0000001!")
def __init__(self, filename): def __init__(self, filename):
self._absolute_start = None
self._filename = filename self._filename = filename
self._fh = None self._fh = None
self._channels = None self._channels = None
@ -34,6 +35,109 @@ class Nd2Parser(object):
self._parse_metadata() self._parse_metadata()
self._z_levels = None 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
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 @property
def _file_handle(self): def _file_handle(self):
if self._fh is None: if self._fh is None:
@ -97,32 +201,6 @@ class Nd2Parser(object):
raise ValueError("Could not parse metadata dimensions!") raise ValueError("Could not parse metadata dimensions!")
return self._dimension_text 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): 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. Images are grouped together if they share the same time index, field of view, and z-level.
@ -150,32 +228,6 @@ class Nd2Parser(object):
channel_offset[channel] = n channel_offset[channel] = n
return channel_offset 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): 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))
@ -188,48 +240,9 @@ class Nd2Parser(object):
return list(range(count)) return list(range(count))
@property @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 :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)

+ 183
- 0
tests/__init__.py View File

@ -0,0 +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)

Loading…
Cancel
Save