diff --git a/Makefile b/Makefile index 29c890b..2a8c0b9 100644 --- a/Makefile +++ b/Makefile @@ -24,4 +24,5 @@ py3: test: build docker run --rm -it jimrybarski/nd2reader python3.4 /opt/nd2reader/tests.py + docker run --rm -it jimrybarski/nd2reader python2.7 /opt/nd2reader/tests.py diff --git a/nd2reader/driver/v3.py b/nd2reader/driver/v3.py index 6fe0300..89270c6 100644 --- a/nd2reader/driver/v3.py +++ b/nd2reader/driver/v3.py @@ -6,6 +6,7 @@ import struct import six from nd2reader.model.image import Image from nd2reader.common.v3 import read_chunk +from nd2reader.exc import NoImageError class V3Driver(object): @@ -47,9 +48,13 @@ class V3Driver(object): 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 + try: + timestamp, image = self._get_raw_image_data(image_group_number, channel_offset, self._metadata.height, self._metadata.width) + except NoImageError: + return None + else: + image.add_params(timestamp, frame_number, fov, channel, z_level) + return image @property def _channel_offset(self): @@ -95,4 +100,4 @@ class V3Driver(object): # them every cycle. if np.any(image_data): return timestamp, Image(image_data) - return None + raise NoImageError diff --git a/nd2reader/exc.py b/nd2reader/exc.py index eda1f2d..f5b55b8 100644 --- a/nd2reader/exc.py +++ b/nd2reader/exc.py @@ -1,2 +1,15 @@ class InvalidVersionError(Exception): + """ + We don't know how to parse the version of ND2 that we were given. + + """ + pass + + +class NoImageError(Exception): + """ + Some apparent images in ND2s are just completely blank placeholders. These are used when the number of images per + cycle are unequal (e.g. if you take fluorescent images every 2 minutes, and bright field images every minute). + + """ pass diff --git a/nd2reader/model/image.py b/nd2reader/model/image.py index e3ee334..39e10a5 100644 --- a/nd2reader/model/image.py +++ b/nd2reader/model/image.py @@ -19,10 +19,11 @@ class Image(np.ndarray): """ 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 + :type timestamp: float + :param frame_number: The order in which this image was taken, with images of different channels/z-levels + at the same field of view treated as being in the same frame. + :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: The name of the color of this image diff --git a/nd2reader/model/metadata.py b/nd2reader/model/metadata.py index 5cb8ca7..11a0164 100644 --- a/nd2reader/model/metadata.py +++ b/nd2reader/model/metadata.py @@ -12,10 +12,22 @@ class Metadata(object): @property def height(self): + """ + The image height in pixels. + + :rtype: int + + """ return self._height @property def width(self): + """ + The image width in pixels. + + :rtype: int + + """ return self._width @property diff --git a/nd2reader/parser/v2.py b/nd2reader/parser/v2.py deleted file mode 100644 index e69de29..0000000 diff --git a/nd2reader/version.py b/nd2reader/version.py index ca50011..cdd4b82 100644 --- a/nd2reader/version.py +++ b/nd2reader/version.py @@ -6,8 +6,8 @@ def get_version(fh): """ Determines what version the ND2 is. - :param filename: the path (absolute or relative) to the ND2 - :type filename: str + :param fh: an open file handle to the ND2 + :type fh: file """ # the first 16 bytes seem to have no meaning, so we skip them diff --git a/tests/driver/driver.py b/tests/driver/driver.py deleted file mode 100644 index 35752e4..0000000 --- a/tests/driver/driver.py +++ /dev/null @@ -1,7 +0,0 @@ -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 index 9add147..c894b61 100644 --- a/tests/driver/version.py +++ b/tests/driver/version.py @@ -1,5 +1,5 @@ import unittest -from nd2reader.driver.version import parse_version +from nd2reader.version import parse_version class VersionTests(unittest.TestCase): diff --git a/nd2reader/driver/v2.py b/tests/model/__init__.py similarity index 100% rename from nd2reader/driver/v2.py rename to tests/model/__init__.py diff --git a/tests/model/image.py b/tests/model/image.py new file mode 100644 index 0000000..2b10757 --- /dev/null +++ b/tests/model/image.py @@ -0,0 +1,42 @@ +from nd2reader.model.image import Image +import numpy as np +import unittest + + +class ImageTests(unittest.TestCase): + """ + Basically just tests that the Image API works and that Images act as Numpy arrays. There's very little going on + here other than simply storing data. + + """ + def setUp(self): + array = np.array([[0, 1, 254], + [45, 12, 9], + [12, 12, 99]]) + self.image = Image(array) + self.image.add_params(1200.314, 17, 2, 'GFP', 1) + + def test_size(self): + self.assertEqual(self.image.height, 3) + self.assertEqual(self.image.width, 3) + + def test_timestamp(self): + self.assertEqual(self.image.timestamp, 1.200314) + + def test_frame_number(self): + self.assertEqual(self.image.frame_number, 17) + + def test_fov(self): + self.assertEqual(self.image.field_of_view, 2) + + def test_channel(self): + self.assertEqual(self.image.channel, 'GFP') + + def test_z_level(self): + self.assertEqual(self.image.z_level, 1) + + def test_slice(self): + subimage = self.image[:2, :2] + expected = np.array([[0, 1], + [45, 12]]) + self.assertTrue(np.array_equal(subimage, expected))