From 7b7bf1ac25e69eb534cfbc40d71c12e71b0cc422 Mon Sep 17 00:00:00 2001 From: Ruben Verweij Date: Wed, 15 Feb 2017 17:33:42 +0100 Subject: [PATCH] Start porting to PIMS framework, cleanup unnecessary files --- .dockerignore | 11 -- CHANGELOG.md | 79 ---------- CONTRIBUTING.md | 18 +-- Dockerfile | 44 ------ Makefile | 37 ----- README.md | 123 ++------------- ftest.py | 7 - functional_tests/FYLM141111001.py | 240 ------------------------------ functional_tests/__init__.py | 0 functional_tests/monocycle.py | 146 ------------------ functional_tests/single.py | 68 --------- nd2reader/__init__.py | 2 +- nd2reader/{main.py => legacy.py} | 0 nd2reader/nd2reader.py | 64 ++++++++ requirements.txt | 3 +- setup.py | 3 +- 16 files changed, 79 insertions(+), 766 deletions(-) delete mode 100644 .dockerignore delete mode 100644 CHANGELOG.md delete mode 100644 Dockerfile delete mode 100644 Makefile delete mode 100644 ftest.py delete mode 100644 functional_tests/FYLM141111001.py delete mode 100644 functional_tests/__init__.py delete mode 100644 functional_tests/monocycle.py delete mode 100644 functional_tests/single.py rename nd2reader/{main.py => legacy.py} (100%) create mode 100644 nd2reader/nd2reader.py diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 85515a5..0000000 --- a/.dockerignore +++ /dev/null @@ -1,11 +0,0 @@ -.gitignore -.git -*.md -*.txt -Dockerfile -Makefile -setup.cfg -env -*.egg-info -build -dist \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index c2ff09b..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,79 +0,0 @@ -## [2.1.1] - 2016-02-15 -### FIXED -- `Image` objects behave properly when passed to numpy functions. Things like `np.mean` will now return scalar values as expected, instead of `Image` objects - -## [2.1.0] - 2016-01-16 -### ADDED -- `select` now supports `start` and `stop` keyword arguments to put bounds on images - -## [2.0.2] - 2016-01-06 -### ADDED -- `Nd2.pixel_microns` gives the width of a pixel - -## [2.0.1] - 2016-01-06 -### FIXED -- Channel name parsing issue -- `select` method works for files with a single frame - -## [2.0.0] - 2015-12-20 -### ADDED -- `select()` method to rapidly iterate over a subset of images matching certain criteria -- We parse metadata relating to the physical camera used to produce the images -- Raw metadata can be accessed conveniently, to allow contributors to find more interesting things to add -- An XML parsing library was added since the raw metadata contains some XML blocks -- The version number is now available in the nd2reader module -- Created a DOI to allow citation of the code - -### FIXED -- Channel names were not always being parsed properly - -### REMOVED -- The `ImageGroup` container object -- The `data` attribute on Images. Images now inherit from ndarray, making this redundant -- The `image_sets` iterator - -## [1.1.4] - 2015-10-27 -### FIXED -- Implemented missing get_image_by_attributes method - -## [1.1.3] - 2015-10-16 -### FIXED -- ND2s with a single image can now be parsed - -## [1.1.2] - 2015-10-09 -### ADDED -- `Image` objects now have a `frame_number` attribute. -- `Nd2` can be used as a context manager -- More unit tests and functional tests - -### CHANGED -- `Image` objects now directly subclass Numpy arrays -- Refactored code to permit parsing of different versions of ND2s, which will allow us to add support for NIS Elements 3.x. - -### DEPRECATED -- The `data` attribute is no longer needed since `Image` is now a Numpy array -- The `image_sets` iterator will be removed in the near future. You should implement this yourself - -## [1.1.1] - 2015-09-02 -### FIXED -- Images returned by indexing would sometimes be skipped when the file contained multiple channels - -### CHANGED -- Dockerfile now installs scikit-image to make visual testing possible - -## [1.1.0] - 2015-06-03 -### ADDED -- Indexing and slicing of images -- Python 3 support -- Dockerfile support for Python 3.4 -- Makefile commands for convenient testing in Docker -- Unit tests - -### CHANGED -- Switched to setuptools to automatically install missing dependencies -- Made the interface for most metadata public -- Refactored some poorly-named things - -## [1.0.0] - 2015-05-23 -### ADDED -- First stable release! diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 192eea0..b336b46 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,20 +15,4 @@ system so everyone can benefit, but for now, they will just be manually run by t ## Contributing Your ND2 files -We always appreciate more ND2s, as they help us find corner cases. Please get in touch using any of the means listed at the top of this page if you'd like to send one in. - -## Docker and Makefile Commands - -A Dockerfile is included to allow testing in a consistent environment. `make build` will build the image for you. Due to the large number of packages that it installs, it often -fails due to a problem with the Debian servers - just try again if this happens. Once that's complete, you can run `make py2` or `make py3` to enter into a Python interpreter in -the container to test things out manually. This assumes you have the directory `~/nd2s` - any ND2 files placed there will be available in the container at `/var/nds2`. You can -view images with scikit-image with something like the code below: - -``` -from nd2reader import Nd2 -from skimage import io - -n = Nd2("/var/nd2s/my.nd2") -io.imshow(n[37]) -io.show() -``` +We always appreciate more ND2s, as they help us find corner cases. Please get in touch using any of the means listed at the top of this page if you'd like to send one in. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 4d27312..0000000 --- a/Dockerfile +++ /dev/null @@ -1,44 +0,0 @@ -# This is just for functional testing. We install scikit-image just as a convenient way to view images. Many other -# packages could easily accomplish this. - -FROM debian:latest -MAINTAINER Jim Rybarski - -RUN mkdir -p /var/nds2 -RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential \ - liblapack-dev \ - libblas-dev \ - libatlas3-base \ - python \ - python3 \ - python-dev \ - python3-dev \ - python-pip \ - python3-pip \ - python-numpy \ - python3-numpy \ - libfreetype6-dev \ - python-matplotlib \ - python3-matplotlib \ - libfreetype6-dev \ - libpng-dev \ - libjpeg-dev \ - pkg-config \ - python-skimage \ - python3-skimage \ - tk \ - tk-dev \ - python-tk \ - python3-tk \ - && pip install -U \ - cython \ - scikit-image \ - xmltodict \ - && pip3 install -U \ - cython \ - scikit-image \ - xmltodict \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /opt/nd2reader diff --git a/Makefile b/Makefile deleted file mode 100644 index 2b090a3..0000000 --- a/Makefile +++ /dev/null @@ -1,37 +0,0 @@ -.PHONY: info build shell py2 py3 test ftest publish - -info: - @echo "" - @echo "Available Make Commands" - @echo "" - @echo "build: builds the image" - @echo "shell: starts a bash shell in the container - @echo "py2: maps ~/Documents/nd2s to /var/nd2s and runs a Python 2.7 interpreter" - @echo "py3: maps ~/Documents/nd2s to /var/nd2s and runs a Python 3.4 interpreter" - @echo "test: runs all unit tests (in Python 3.4)" - @echo "ftest: runs all functional tests (requires specific ND2 files that are not publicly available" - @echo "publish: publishes the code base to PyPI (maintainers only)" - @echo "" - -build: - docker build -t jimrybarski/nd2reader . - -shell: - xhost local:root; docker run --rm -v $(CURDIR):/opt/nd2reader -v ~/nd2s:/var/nd2s -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$(DISPLAY) -it jimrybarski/nd2reader bash - -py2: - xhost local:root; docker run --rm -v $(CURDIR):/opt/nd2reader -v ~/nd2s:/var/nd2s -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$(DISPLAY) -it jimrybarski/nd2reader python2.7 - -py3: - xhost local:root; docker run --rm -v $(CURDIR):/opt/nd2reader -v ~/nd2s:/var/nd2s -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$(DISPLAY) -it jimrybarski/nd2reader python3.4 - -test: build - docker run --rm -v $(CURDIR):/opt/nd2reader -it jimrybarski/nd2reader python3.4 test.py - docker run --rm -v $(CURDIR):/opt/nd2reader -it jimrybarski/nd2reader python2.7 test.py - -ftest: build - xhost local:root; docker run --rm -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$(DISPLAY) -v $(CURDIR):/opt/nd2reader -v ~/nd2s:/var/nd2s -it jimrybarski/nd2reader python3.4 /opt/nd2reader/ftest.py - xhost local:root; docker run --rm -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=unix$(DISPLAY) -v $(CURDIR):/opt/nd2reader -v ~/nd2s:/var/nd2s -it jimrybarski/nd2reader python2.7 /opt/nd2reader/ftest.py - -publish: - python setup.py sdist upload -r pypi diff --git a/README.md b/README.md index 9a1ed6a..034d5b1 100644 --- a/README.md +++ b/README.md @@ -3,121 +3,23 @@ ### About `nd2reader` is a pure-Python package that reads images produced by NIS Elements 4.0+. It has only been definitively tested on NIS Elements 4.30.02 Build 1053. Support for older versions is being actively worked on. - -.nd2 files contain images and metadata, which can be split along multiple dimensions: time, fields of view (xy-plane), focus (z-plane), and filter channel. - -`nd2reader` loads images as Numpy arrays, which makes it trivial to use with the image analysis packages such as `scikit-image` and `OpenCV`. +The reader is written in the [pims](https://github.com/soft-matter/pims) framework, enabling easy access to multidimensional files, lazy slicing, and nice display in IPython. ### Installation -If you don't already have the packages `numpy`, `six` and `xmltodict`, they will be installed automatically: - -`pip3 install nd2reader` for Python 3.x - -`pip install nd2reader` for Python 2.x - +If you don't already have the packages `numpy`, `pims`, `six` and `xmltodict`, they will be installed automatically if you use the `setup.py` script. `nd2reader` is an order of magnitude faster in Python 3. I recommend using it unless you have no other choice. ### ND2s -A quick summary of ND2 metadata can be obtained as shown below. -```python ->>> import nd2reader ->>> nd2 = nd2reader.Nd2("/path/to/my_images.nd2") ->>> nd2 - -Created: 2014-11-11 15:59:19 -Image size: 1280x800 (HxW) -Image cycles: 636 -Channels: 'brightfield', 'GFP' -Fields of View: 8 -Z-Levels: 3 - -``` - -You can iterate over each image in the order they were acquired: - -```python -import nd2reader -nd2 = nd2reader.Nd2("/path/to/my_images.nd2") -for image in nd2: - do_something(image) -``` - -`Image` objects are just Numpy arrays with some extra metadata bolted on: - -```python ->>> image = nd2[20] ->>> image -array([[1894, 1949, 1941, ..., 2104, 2135, 2114], - [1825, 1846, 1848, ..., 1994, 2149, 2064], - [1909, 1820, 1821, ..., 1995, 1952, 2062], - ..., - [3487, 3512, 3594, ..., 3603, 3643, 3492], - [3642, 3475, 3525, ..., 3712, 3682, 3609], - [3687, 3777, 3738, ..., 3784, 3870, 4008]], dtype=uint16) - ->>> image.timestamp -10.1241241248 ->>> image.frame_number -11 ->>> image.field_of_view -6 ->>> image.channel -'GFP' ->>> image.z_level -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, but you can -set a range with the `start` and `stop` arguments: - -```python -for image in nd2.select(channels="GFP", fields_of_view=(1, 2, 7)): - # gets all GFP images in fields of view 1, 2 and 7, regardless of z-level or frame - do_something(image) - -for image in nd2.select(z_levels=(0, 1), start=12, stop=3000): - # gets images of any channel or field of view, with z-level 0 or 1, between images 12 and 3000 - do_something(image) -``` - -Slicing is also supported and is extremely memory efficient, as images are only read when directly accessed: - -```python -for image in nd2[50:433]: - do_something(image) - -# get every other image in the first 100 images -for image in nd2[:100:2]: - do_something(image) - -# iterate backwards over every image -for image in nd2[::-1]: - do_something(image) -``` - -You can also just index a single image: +`nd2reader` follows the [pims](https://github.com/soft-matter/pims) framework. To open a file: ```python -# gets the 18th image -my_important_image = nd2[17] +from nd2reader import ND2Reader +images = ND2Reader('my_directory/example.nd2') ``` -The `Nd2` object has some programmatically-accessible metadata: - -```python ->>> nd2.height # in pixels -1280 ->>> nd2.width # in pixels -800 ->>> len(nd2) # the number of images -30528 ->>> nd2.pixel_microns # the width of a pixel in microns -0.22 -``` +After opening the file, all `pims` features are supported. Please refer to the [documentation](http://soft-matter.github.io/pims/). ### Contributing @@ -129,15 +31,8 @@ for more information. If this fails to work exactly as expected, please open an [issue](https://github.com/rbnvrw/nd2reader/issues). If you get an unhandled exception, please paste the entire stack trace into the issue as well. -### Citation - -You can cite nd2reader in your research if you want: - -``` -Rybarski, Jim (2015): nd2reader. figshare. -http://dx.doi.org/10.6084/m9.figshare.1619960 -``` - ### Acknowledgments -Original version by Jim Rybarski. Support for the development of this package was provided by the [Finkelstein Laboratory](http://finkelsteinlab.org/). +PIMS modified version by Ruben Verweij. + +Original version by Jim Rybarski. Support for the development of this package was partially provided by the [Finkelstein Laboratory](http://finkelsteinlab.org/). diff --git a/ftest.py b/ftest.py deleted file mode 100644 index d429b6a..0000000 --- a/ftest.py +++ /dev/null @@ -1,7 +0,0 @@ -import unittest -from functional_tests.FYLM141111001 import FYLM141111Tests -from functional_tests.single import SingleTests -from functional_tests.monocycle import Monocycle1Tests, Monocycle2Tests, OneTests - -if __name__ == '__main__': - unittest.main() diff --git a/functional_tests/FYLM141111001.py b/functional_tests/FYLM141111001.py deleted file mode 100644 index d1e857a..0000000 --- a/functional_tests/FYLM141111001.py +++ /dev/null @@ -1,240 +0,0 @@ -""" -These tests require that you have a specific ND2 file created by the developer of nd2reader. You will never need to -run them unless you're Jim Rybarski. - -""" -from nd2reader import Nd2 -from skimage import io -import numpy as np -from datetime import datetime -import unittest -import time - - -class FYLM141111Tests(unittest.TestCase): - def setUp(self): - self.nd2 = Nd2("/var/nd2s/FYLM-141111-001.nd2") - - def tearDown(self): - self.nd2.close() - - def test_shape(self): - self.assertEqual(self.nd2.height, 1280) - self.assertEqual(self.nd2.width, 800) - - def test_date(self): - self.assertEqual(self.nd2.date, datetime(2014, 11, 11, 15, 59, 19)) - - @unittest.skip("This will fail until we address issue #59") - def test_length(self): - self.assertEqual(len(self.nd2), 17808) - - def test_frames(self): - self.assertEqual(len(self.nd2.frames), 636) - - def test_fovs(self): - self.assertEqual(len(self.nd2.fields_of_view), 8) - - def test_channels(self): - self.assertTupleEqual(tuple(sorted(self.nd2.channels)), ('', 'GFP')) - - def test_z_levels(self): - self.assertTupleEqual(tuple(self.nd2.z_levels), (0, 1, 2)) - - def test_pixel_size(self): - self.assertGreater(self.nd2.pixel_microns, 0.0) - - def test_image(self): - image = self.nd2[14] - self.assertEqual(image.field_of_view, 2) - self.assertEqual(image.frame_number, 0) - self.assertAlmostEqual(image.timestamp, 19.0340758) - self.assertEqual(image.channel, '') - self.assertEqual(image.z_level, 1) - self.assertEqual(image.height, self.nd2.height) - self.assertEqual(image.width, self.nd2.width) - - def test_last_image(self): - image = self.nd2[30526] - self.assertEqual(image.frame_number, 635) - - def test_bad_image(self): - image = self.nd2[13] - self.assertIsNone(image) - - def test_iteration(self): - images = [image for image in self.nd2[:10]] - self.assertEqual(len(images), 10) - - def test_iteration_step(self): - images = [image for image in self.nd2[:10:2]] - self.assertEqual(len(images), 5) - - def test_iteration_backwards(self): - images = [image for image in self.nd2[:10:-1]] - self.assertEqual(len(images), 10) - - def test_get_image_by_attribute_ok(self): - image = self.nd2.get_image(4, 0, 'GFP', 1) - self.assertIsNotNone(image) - image = self.nd2.get_image(4, 0, '', 0) - self.assertIsNotNone(image) - image = self.nd2.get_image(4, 0, '', 1) - self.assertIsNotNone(image) - - def test_images(self): - self.assertTupleEqual((self.nd2[0].z_level, self.nd2[0].channel), (0, '')) - self.assertIsNone(self.nd2[1]) - self.assertTupleEqual((self.nd2[2].z_level, self.nd2[2].channel), (1, '')) - self.assertTupleEqual((self.nd2[3].z_level, self.nd2[3].channel), (1, 'GFP')) - self.assertTupleEqual((self.nd2[4].z_level, self.nd2[4].channel), (2, '')) - self.assertIsNone(self.nd2[5]) - self.assertTupleEqual((self.nd2[6].z_level, self.nd2[6].channel), (0, '')) - self.assertIsNone(self.nd2[7]) - self.assertTupleEqual((self.nd2[8].z_level, self.nd2[8].channel), (1, '')) - self.assertTupleEqual((self.nd2[9].z_level, self.nd2[9].channel), (1, 'GFP')) - self.assertTupleEqual((self.nd2[10].z_level, self.nd2[10].channel), (2, '')) - self.assertIsNone(self.nd2[11]) - self.assertTupleEqual((self.nd2[12].z_level, self.nd2[12].channel), (0, '')) - self.assertIsNone(self.nd2[13]) - self.assertTupleEqual((self.nd2[14].z_level, self.nd2[14].channel), (1, '')) - self.assertTupleEqual((self.nd2[15].z_level, self.nd2[15].channel), (1, 'GFP')) - self.assertTupleEqual((self.nd2[16].z_level, self.nd2[16].channel), (2, '')) - self.assertIsNone(self.nd2[17]) - self.assertTupleEqual((self.nd2[18].z_level, self.nd2[18].channel), (0, '')) - self.assertIsNone(self.nd2[19]) - self.assertIsNone(self.nd2[47]) - self.assertTupleEqual((self.nd2[48].z_level, self.nd2[48].channel), (0, '')) - self.assertIsNone(self.nd2[49]) - self.assertTupleEqual((self.nd2[50].z_level, self.nd2[50].channel), (1, '')) - self.assertIsNone(self.nd2[51]) - self.assertTupleEqual((self.nd2[52].z_level, self.nd2[52].channel), (2, '')) - self.assertIsNone(self.nd2[53]) - self.assertTupleEqual((self.nd2[54].z_level, self.nd2[54].channel), (0, '')) - - def test_get_image_by_attribute_none(self): - # Should handle missing images without an exception - image = self.nd2.get_image(4, 0, 'GFP', 0) - self.assertIsNone(image) - - def test_index(self): - # Do indexes get added to images properly? - for n, image in enumerate(self.nd2): - if image is not None: - self.assertEqual(n, image.index) - if n > 50: - break - - 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): - if image is not None and image.channel == 'GFP': - manual_images.append(image) - - filter_images = [] - for image in self.nd2.select(channels='GFP'): - filter_images.append(image) - if len(filter_images) == len(manual_images): - break - - self.assertEqual(len(manual_images), len(filter_images)) - self.assertGreater(len(manual_images), 0) - for a, b in zip(manual_images, filter_images): - self.assertTrue(np.array_equal(a, b)) - self.assertEqual(a.index, b.index) - self.assertEqual(a.field_of_view, b.field_of_view) - self.assertEqual(a.channel, b.channel) - - def test_select_order_all(self): - # If we select every possible image using select(), we should just get every image in order - n = 0 - for image in self.nd2.select(channels=['', '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: - break - n += 1 - self.assertTrue(np.array_equal(image, indexed_image)) - n += 1 - if n > 100: - break - - def test_select_order_subset(self): - # 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.select(channels='', z_levels=[0, 1], fields_of_view=[1, 2, 4]): - self.assertGreater(image.index, n) - self.assertEqual(image.channel, '') - self.assertIn(image.field_of_view, (1, 2, 4)) - self.assertIn(image.z_level, (0, 1)) - n = image.index - if n > 100: - break - - def test_select_start(self): - count = 0 - for _ in self.nd2.select(channels='GFP', start=29000): - count += 1 - self.assertEqual(127, count) - - def test_select_stop(self): - count = 0 - for _ in self.nd2.select(channels='GFP', stop=20): - count += 1 - self.assertEqual(count, 3) - - def test_select_start_stop(self): - count = 0 - for _ in self.nd2.select(channels='GFP', start=10, stop=20): - count += 1 - self.assertEqual(count, 1) - - def test_select_start_stop_brightfield(self): - count = 0 - for _ in self.nd2.select(channels='', start=10, stop=20): - count += 1 - self.assertEqual(count, 5) - - def test_select_faster(self): - select_count = 0 - select_start = time.time() - for i in self.nd2.select(channels='GFP', start=10, stop=50): - if i is not None and i.channel == 'GFP': - select_count += 1 - select_duration = time.time() - select_start - - direct_count = 0 - direct_start = time.time() - for i in self.nd2[10:50]: - if i is not None and i.channel == 'GFP': - direct_count += 1 - direct_duration = time.time() - direct_start - self.assertEqual(select_count, direct_count) - self.assertGreater(direct_duration, select_duration) - - def test_pixel_microns(self): - self.assertEqual(round(self.nd2.pixel_microns, 2), 0.22) - - def test_numpy_operations(self): - # just to make sure we can do this kind of thing and get scalars - self.assertTrue(0 < np.mean(self.nd2[0]) < np.sum(self.nd2[0])) - - def test_numpy_mean(self): - # make sure we get the right value and type - expected_mean = 17513.053581054686 - mean = np.mean(self.nd2[0]) - self.assertEqual(type(mean), np.float64) - self.assertAlmostEqual(expected_mean, mean) - - def test_subtract_images(self): - # just to prove we can really treat Image like an array - diff = self.nd2[0] - self.nd2[2] - self.assertTrue(np.any(diff)) - - def test_show(self): - io.imshow(self.nd2[0]) - io.show() - self.assertTrue(True) diff --git a/functional_tests/__init__.py b/functional_tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/functional_tests/monocycle.py b/functional_tests/monocycle.py deleted file mode 100644 index b1830da..0000000 --- a/functional_tests/monocycle.py +++ /dev/null @@ -1,146 +0,0 @@ -""" -Tests on ND2s that have 1 or 2 cycles only. This is unlike the ND2s I work with typically, which are all done over very long periods of time. - -""" -from nd2reader import Nd2 -import numpy as np -import unittest - - -class Monocycle1Tests(unittest.TestCase): - def setUp(self): - self.nd2 = Nd2("/var/nd2s/simone1.nd2") - - def tearDown(self): - self.nd2.close() - - def test_channels(self): - self.assertListEqual(self.nd2.channels, ['Cy3Narrow', 'TxRed-modified', 'FITC', 'DAPI']) - - def test_pixel_size(self): - self.assertGreater(self.nd2.pixel_microns, 0.0) - - def test_select(self): - manual_images = [] - for _, image in zip(range(20), self.nd2): - if image is not None and image.channel == 'FITC': - manual_images.append(image) - - filter_images = [] - for image in self.nd2.select(channels='FITC'): - filter_images.append(image) - if len(filter_images) == len(manual_images): - break - - self.assertEqual(len(manual_images), len(filter_images)) - self.assertGreater(len(manual_images), 0) - for a, b in zip(manual_images, filter_images): - self.assertTrue(np.array_equal(a, b)) - self.assertEqual(a.index, b.index) - self.assertEqual(a.field_of_view, b.field_of_view) - self.assertEqual(a.channel, b.channel) - - def test_select_order_all(self): - # If we select every possible image using select(), we should just get every image in order - n = 0 - for image in self.nd2.select(channels=['Cy3Narrow', 'DAPI', 'FITC', 'TxRed-modified'], - z_levels=list(range(35)), - fields_of_view=list(range(5))): - while True: - indexed_image = self.nd2[n] - if indexed_image is not None: - break - n += 1 - self.assertTrue(np.array_equal(image, indexed_image)) - n += 1 - if n > 100: - # Quit after the first hundred images just to save time. - # If there's a problem, we'll have seen it by now. - break - - def test_select_order_subset(self): - # 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.select(channels='FITC', - z_levels=[0, 1], - fields_of_view=[1, 2, 4]): - self.assertGreater(image.index, n) - self.assertEqual(image.channel, 'FITC') - self.assertIn(image.field_of_view, (1, 2, 4)) - self.assertIn(image.z_level, (0, 1)) - n = image.index - if n > 100: - break - - -class Monocycle2Tests(unittest.TestCase): - def setUp(self): - self.nd2 = Nd2("/var/nd2s/hawkjo.nd2") - - def tearDown(self): - self.nd2.close() - - def test_pixel_size(self): - self.assertGreater(round(self.nd2.pixel_microns, 2), 0.26) - - def test_select(self): - manual_images = [] - for _, image in zip(range(20), self.nd2): - if image is not None and image.channel == 'HHQ 500 LP 1': - manual_images.append(image) - - filter_images = [] - for image in self.nd2.select(channels='HHQ 500 LP 1'): - filter_images.append(image) - if len(filter_images) == len(manual_images): - break - - self.assertEqual(len(manual_images), len(filter_images)) - self.assertGreater(len(manual_images), 0) - for a, b in zip(manual_images, filter_images): - self.assertTrue(np.array_equal(a, b)) - self.assertEqual(a.index, b.index) - self.assertEqual(a.field_of_view, b.field_of_view) - self.assertEqual(a.channel, b.channel) - - def test_select_order_all(self): - # If we select every possible image using select(), we should just get every image in order - n = 0 - for image in self.nd2.select(channels=['HHQ 500 LP 1', 'HHQ 500 LP 2'], - z_levels=[0], - fields_of_view=list(range(100))): - while True: - indexed_image = self.nd2[n] - if indexed_image is not None: - break - n += 1 - self.assertTrue(np.array_equal(image, indexed_image)) - n += 1 - if n > 100: - # Quit after the first hundred images just to save time. - # If there's a problem, we'll have seen it by now. - break - - def test_select_order_subset(self): - # 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.select(channels='HHQ 500 LP 2', - z_levels=[0], - fields_of_view=[1, 2, 4]): - self.assertGreater(image.index, n) - self.assertEqual(image.channel, 'HHQ 500 LP 2') - self.assertIn(image.field_of_view, (1, 2, 4)) - self.assertEqual(image.z_level, 0) - n = image.index - if n > 100: - break - - -class OneTests(unittest.TestCase): - def test_opens(self): - # just testing that this doesn't throw an exception - nd2 = Nd2("/var/nd2s/001.nd2") - self.assertIsNotNone(nd2) - nd2.close() diff --git a/functional_tests/single.py b/functional_tests/single.py deleted file mode 100644 index 519149e..0000000 --- a/functional_tests/single.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -These tests require that you have a specific ND2 file created by the developer of nd2reader. You will never need to -run them unless you're Jim Rybarski. - -""" -from nd2reader import Nd2 -from datetime import datetime -import unittest - - -class SingleTests(unittest.TestCase): - def setUp(self): - self.nd2 = Nd2("/var/nd2s/single.nd2") - - def tearDown(self): - self.nd2.close() - - def test_shape(self): - self.assertEqual(self.nd2.height, 512) - self.assertEqual(self.nd2.width, 512) - - def test_date(self): - self.assertEqual(self.nd2.date, datetime(2015, 10, 15, 9, 33, 5)) - - def test_length(self): - self.assertEqual(len(self.nd2), 1) - - def test_channels(self): - self.assertEqual(self.nd2.channels, ['Quad Band 2']) - - def test_pixel_size(self): - self.assertGreater(self.nd2.pixel_microns, 0.0) - - def test_actual_length(self): - count = 0 - for image in self.nd2: - if image is not None: - count += 1 - self.assertEqual(len(self.nd2), count) - - def test_frames(self): - self.assertEqual(len(self.nd2.frames), 1) - - def test_fovs(self): - self.assertEqual(len(self.nd2.fields_of_view), 1) - - def test_z_levels(self): - self.assertTupleEqual(tuple(self.nd2.z_levels), (0,)) - - def test_image(self): - image = self.nd2[0] - self.assertIsNotNone(image) - - def test_iteration(self): - images = [image for image in self.nd2] - self.assertEqual(len(images), 1) - - def test_iteration_step(self): - images = [image for image in self.nd2[::2]] - self.assertEqual(len(images), 1) - - def test_iteration_backwards(self): - images = [image for image in self.nd2[::-1]] - self.assertEqual(len(images), 1) - - def test_select_bounds_wrong(self): - images = [i for i in self.nd2.select(start=0, stop=12481247)] - self.assertEqual(len(images), 1) diff --git a/nd2reader/__init__.py b/nd2reader/__init__.py index 6184c21..7a0be5f 100644 --- a/nd2reader/__init__.py +++ b/nd2reader/__init__.py @@ -1,3 +1,3 @@ -from nd2reader.main import Nd2 +from nd2reader.nd2reader import ND2Reader __version__ = '2.1.3' diff --git a/nd2reader/main.py b/nd2reader/legacy.py similarity index 100% rename from nd2reader/main.py rename to nd2reader/legacy.py diff --git a/nd2reader/nd2reader.py b/nd2reader/nd2reader.py new file mode 100644 index 0000000..3fbca48 --- /dev/null +++ b/nd2reader/nd2reader.py @@ -0,0 +1,64 @@ +from pims import FramesSequenceND, Frame +import numpy as np + +from nd2reader.exc import NoImageError +from nd2reader.parser import get_parser +from nd2reader.version import get_version +import six + + +class ND2Reader(FramesSequenceND): + """ + PIMS wrapper for the ND2 reader + """ + + def __init__(self, filename): + self.filename = filename + + # first use the parser to parse the file + self._fh = open(filename, "rb") + major_version, minor_version = get_version(self._fh) + self._parser = get_parser(self._fh, major_version, minor_version) + self._metadata = self._parser.metadata + self._roi_metadata = self._parser.roi_metadata + + # Set data type + bit_depth = self._parser.raw_metadata.image_attributes[six.b('SLxImageAttributes')][six.b('uiBpcInMemory')] + if bit_depth <= 16: + self._dtype = np.float16 + elif bit_depth <= 32: + self._dtype = np.float32 + else: + self._dtype = np.float64 + + # Setup the axes + self._init_axis('x', self._metadata.width) + self._init_axis('y', self._metadata.height) + self._init_axis('c', len(self._metadata.channels)) + self._init_axis('t', len(self._metadata.frames)) + self._init_axis('z', len(self._metadata.z_levels)) + + def close(self): + if self._fh is not None: + self._fh.close() + + def get_frame_2D(self, c, t, z): + """ + Gets a given frame using the parser + :param c: + :param t: + :param z: + :return: + """ + c_name = self._metadata.channels[c] + try: + image = self._parser.driver.get_image_by_attributes(t, 0, c_name, z, self._metadata.width, + self._metadata.height) + except (TypeError, NoImageError): + return Frame([]) + else: + return Frame(image, frame_no=image.frame_number) + + @property + def pixel_type(self): + return self._dtype diff --git a/requirements.txt b/requirements.txt index 1cb2326..b6d1992 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ numpy>=1.9.2 six>=1.4 -xmltodict>=0.9.2 \ No newline at end of file +xmltodict>=0.9.2 +pims>=0.3.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 89d5aa3..fa0f64c 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,8 @@ if __name__ == '__main__': install_requires=[ 'numpy>=1.6.2, <2.0', 'six>=1.4, <2.0', - 'xmltodict>=0.9.2, <1.0' + 'xmltodict>=0.9.2, <1.0', + 'pims>=0.3.0' ], version=VERSION, description='A tool for reading ND2 files produced by NIS Elements',