From 47966b75dd189495bd840efcc19a0d1c25d2a2d0 Mon Sep 17 00:00:00 2001 From: Jim Rybarski Date: Sat, 5 Dec 2015 16:41:09 -0600 Subject: [PATCH] resolves #129: renamed filter() to select() --- Dockerfile | 8 +- README.md | 9 +++ functional_tests/FYLM141111001.py | 12 +-- nd2reader/interface.py | 123 +++++++++++++++++------------- tests/model/image.py | 2 +- 5 files changed, 90 insertions(+), 64 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4002514..4d27312 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,10 +31,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ tk-dev \ python-tk \ python3-tk \ - && pip install -U cython \ + && pip install -U \ + cython \ scikit-image \ - && pip3 install -U cython \ + xmltodict \ + && pip3 install -U \ + cython \ scikit-image \ + xmltodict \ && rm -rf /var/lib/apt/lists/* WORKDIR /opt/nd2reader diff --git a/README.md b/README.md index fff9f55..5248b5b 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,15 @@ array([[1894, 1949, 1941, ..., 2104, 2135, 2114], 0 ``` +If you only want to view images that meet certain criteria, you can use `select()`. It's much faster than iterating +and checking attributes of images manually. You can specify scalars or lists of values. Criteria that aren't specified +default to every possible value. Currently, slicing and selecting can't be done at the same time: + +```python +for image in nd2.select(channels="GFP", fields_of_view=(1, 2, 7)): + do_something(image) +``` + Slicing is also supported and is extremely memory efficient, as images are only read when directly accessed: ```python diff --git a/functional_tests/FYLM141111001.py b/functional_tests/FYLM141111001.py index 8b77238..ffffbdb 100644 --- a/functional_tests/FYLM141111001.py +++ b/functional_tests/FYLM141111001.py @@ -120,8 +120,8 @@ class FunctionalTests(unittest.TestCase): if n > 50: break - def test_filter(self): - # If we take the first 20 GFP images, they should be identical to the first 20 items iterated from filter() + def test_select(self): + # If we take the first 20 GFP images, they should be identical to the first 20 items iterated from select() # if we set our criteria to just "GFP" manual_images = [] for _, image in zip(range(20), self.nd2): @@ -129,7 +129,7 @@ class FunctionalTests(unittest.TestCase): manual_images.append(image) filter_images = [] - for image in self.nd2.filter(channels='GFP'): + for image in self.nd2.select(channels='GFP'): filter_images.append(image) if len(filter_images) == len(manual_images): break @@ -137,9 +137,9 @@ class FunctionalTests(unittest.TestCase): self.assertTrue(np.array_equal(a, b)) def test_filter_order_all(self): - # If we select every possible image using filter(), we should just get every image in order + # If we select every possible image using select(), we should just get every image in order n = 0 - for image in self.nd2.filter(channels=['BF', 'GFP'], z_levels=[0, 1, 2], fields_of_view=list(range(8))): + for image in self.nd2.select(channels=['BF', 'GFP'], z_levels=[0, 1, 2], fields_of_view=list(range(8))): while True: indexed_image = self.nd2[n] if indexed_image is not None: @@ -154,7 +154,7 @@ class FunctionalTests(unittest.TestCase): # Test that images are always yielded in increasing order. This guarantees that no matter what subset of images # we're filtering, we still get them in the chronological order they were acquired n = -1 - for image in self.nd2.filter(channels='BF', z_levels=[0, 1], fields_of_view=[1, 2, 4]): + for image in self.nd2.select(channels='BF', z_levels=[0, 1], fields_of_view=[1, 2, 4]): self.assertGreater(image.index, n) self.assertEqual(image.channel, 'BF') self.assertIn(image.field_of_view, (1, 2, 4)) diff --git a/nd2reader/interface.py b/nd2reader/interface.py index 3d12743..c67353f 100644 --- a/nd2reader/interface.py +++ b/nd2reader/interface.py @@ -15,13 +15,6 @@ class Nd2(object): self._parser = get_parser(self._fh, major_version, minor_version) self._metadata = self._parser.metadata - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - if self._fh is not None: - self._fh.close() - def __repr__(self): return "\n".join(["" % self._filename, "Created: %s" % (self.date if self.date is not None else "Unknown"), @@ -32,6 +25,13 @@ class Nd2(object): "Z-Levels: %s" % len(self.z_levels) ]) + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if self._fh is not None: + self._fh.close() + def __len__(self): """ This should be the total number of images in the ND2, but it may be inaccurate. If the ND2 contains a @@ -61,36 +61,48 @@ class Nd2(object): return self._slice(item.start, item.stop, item.step) raise IndexError - def _slice(self, start, stop, step): + def select(self, fields_of_view=None, channels=None, z_levels=None): """ - Allows for iteration over a selection of the entire dataset. + Iterates over images matching the given criteria. This can be 2-10 times faster than manually iterating over + the Nd2 and checking the attributes of each image, as this method skips disk reads for any images that don't + meet the criteria. - :type start: int - :type stop: int - :type step: int - :rtype: nd2reader.model.Image() + :type fields_of_view: int or tuple or list + :type channels: str or tuple or list + :type z_levels: int or tuple or list """ - start = start if start is not None else 0 - step = step if step is not None else 1 - stop = stop if stop is not None else len(self) - # This weird thing with the step allows you to iterate backwards over the images - for i in range(start, stop)[::step]: - yield self[i] + fields_of_view = self._to_list(fields_of_view, self.fields_of_view) + channels = self._to_list(channels, self.channels) + z_levels = self._to_list(z_levels, self.z_levels) + + for frame in self.frames: + for f in fields_of_view: + for z in z_levels: + for c in channels: + image = self.get_image(frame, f, c, z) + if image is not None: + yield image @property - def camera_settings(self): - return self._parser.camera_metadata - + def height(self): + """ + The height of each image in pixels. + + :rtype: int + + """ + return self._metadata.height + @property - def date(self): + def width(self): """ - The date and time that the acquisition began. Not guaranteed to have been recorded. + The width of each image in pixels. - :rtype: datetime.datetime() or None + :rtype: int """ - return self._metadata.date + return self._metadata.width @property def z_levels(self): @@ -140,24 +152,18 @@ class Nd2(object): return self._metadata.frames @property - def height(self): - """ - The height of each image in pixels. - - :rtype: int - - """ - return self._metadata.height - + def camera_settings(self): + return self._parser.camera_metadata + @property - def width(self): + def date(self): """ - The width of each image in pixels. + The date and time that the acquisition began. Not guaranteed to have been recorded. - :rtype: int + :rtype: datetime.datetime() or None """ - return self._metadata.width + return self._metadata.date def get_image(self, frame_number, field_of_view, channel_name, z_level): """ @@ -182,28 +188,35 @@ class Nd2(object): self.height, self.width) - def filter(self, fields_of_view=None, channels=None, z_levels=None): + def _slice(self, start, stop, step): """ - Iterates over images matching the given criteria. This can be 2-10 times faster than manually iterating over - the Nd2 and checking the attributes of each image, as this method will not read from disk until a valid image is - found. + Allows for iteration over a selection of the entire dataset. - """ - fields_of_view = self._to_list(fields_of_view, self.fields_of_view) - channels = self._to_list(channels, self.channels) - z_levels = self._to_list(z_levels, self.z_levels) + :type start: int + :type stop: int + :type step: int + :rtype: nd2reader.model.Image() - for frame in self.frames: - for f in fields_of_view: - for z in z_levels: - for c in channels: - image = self.get_image(frame, f, c, z) - if image is not None: - yield image + """ + start = start if start is not None else 0 + step = step if step is not None else 1 + stop = stop if stop is not None else len(self) + # This weird thing with the step allows you to iterate backwards over the images + for i in range(start, stop)[::step]: + yield self[i] def _to_list(self, value, default): + """ + Idempotently converts a value to a tuple. This allows users to pass in scalar values and iterables to + select(), which is more ergonomic than having to remember to pass in single-member lists + + :type value: int or str or tuple or list + :type default: tuple or list + :rtype: tuple + + """ value = default if value is None else value - return [value] if isinstance(value, int) or isinstance(value, six.string_types) else value + return (value,) if isinstance(value, int) or isinstance(value, six.string_types) else tuple(value) def close(self): """ diff --git a/tests/model/image.py b/tests/model/image.py index 2b10757..08531d7 100644 --- a/tests/model/image.py +++ b/tests/model/image.py @@ -14,7 +14,7 @@ class ImageTests(unittest.TestCase): [45, 12, 9], [12, 12, 99]]) self.image = Image(array) - self.image.add_params(1200.314, 17, 2, 'GFP', 1) + self.image.add_params(1, 1200.314, 17, 2, 'GFP', 1) def test_size(self): self.assertEqual(self.image.height, 3)