diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..dfe4736 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +## [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 +- 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/README.md b/README.md index d17653f..dd0763d 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,58 @@ Fields of View: 8 Z-Levels: 3 ``` -### Simple Iteration +You can also get some metadata about the nd2 programatically: -For most cases, you'll just want to iterate over each image: +```python +>>> nd2.height +1280 +>>> nd2.width +800 +>>> len(nd2) +30528 +``` + +### Images + +`nd2reader` will always return an `Image` object, which contains some metadata about the image as well as the +raw pixel data itself. Images are always a 16-bit grayscale image. The `data` attribute holds the numpy array +with the image data: + +```python +>>> image = nd2[20] +>>> print(image.data) +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) +``` + +You can get a quick summary of image data by examining the `Image` object: + +```python +>>> image + +1280x800 (HxW) +Timestamp: 1699.79478134 +Field of View: 2 +Channel: GFP +Z-Level: 1 +``` + +Or you can access it programmatically: + +```python +image = nd2[0] +print(image.timestamp) +print(image.field_of_view) +print(image.channel) +print(image.z_level) +``` + +Often, you may want to just iterate over each image: ```python import nd2reader @@ -48,10 +97,42 @@ for image in nd2: do_something(image.data) ``` +You can also get an image directly by indexing. Here, we look at the 38th image: + +```python +>>> nd2[37] + +1280x800 (HxW) +Timestamp: 1699.79478134 +Field of View: 2 +Channel: GFP +Z-Level: 1 +``` + +Slicing is also supported and is extremely memory efficient, as images are only read when directly accessed: + +```python +my_subset = nd2[50:433] +for image in my_subset: + do_something(image.data) +``` + +Step sizes are also accepted: + +```python +for image in nd2[:100:2]: + # gets every other image in the first 100 images + do_something(image.data) + +for image in nd2[::-1]: + # iterate backwards over every image, if you're into that kind of thing + do_something_image.data) +``` + ### Image Sets If you have complicated hierarchical data, it may be easier to use image sets, which groups images together if they -share the same time index and field of view: +share the same time index (not timestamp!) and field of view: ```python import nd2reader @@ -59,53 +140,26 @@ nd2 = nd2reader.Nd2("/path/to/my_complicated_images.nd2") for image_set in nd2.image_sets: # you can select images by channel gfp_image = image_set.get("GFP") - do_something_gfp_related(gfp_image) + do_something_gfp_related(gfp_image.data) # you can also specify the z-level. this defaults to 0 if not given out_of_focus_image = image_set.get("Bright Field", z_level=1) - do_something_out_of_focus_related(out_of_focus_image) + do_something_out_of_focus_related(out_of_focus_image.data) ``` -### Direct Image Access +To get an image from an image set, you must specify a channel. It defaults to the 0th z-level, so if you have +more than one z-level you will need to specify it when using `get`: -There is a method, `get_image`, which allows random access to images. This might not always return an image, however, -if you acquired different numbers of images in each cycle of a program. For example, if you acquire GFP images every -other minute, but acquire bright field images every minute, `get_image` will return `None` at certain time indexes. - -### Images +```python +image = image_set.get("YFP") +image = image_set.get("YFP", z_level=2) +``` -`Image` objects provide several pieces of useful data. +You can also see how many images are in your image set: ```python ->>> import nd2reader ->>> nd2 = nd2reader.Nd2("/path/to/my_images.nd2") ->>> image = nd2.get_image(14, 2, "GFP", 1) ->>> image.data -array([[1809, 1783, 1830, ..., 1923, 1920, 1914], - [1687, 1855, 1792, ..., 1986, 1903, 1889], - [1758, 1901, 1849, ..., 1911, 2010, 1954], - ..., - [3363, 3370, 3570, ..., 3565, 3601, 3459], - [3480, 3428, 3328, ..., 3542, 3461, 3575], - [3497, 3666, 3635, ..., 3817, 3867, 3779]]) ->>> image.channel -'GFP' ->>> image.timestamp -1699.7947813408175 ->>> image.field_of_view -2 ->>> image.z_level -1 - -# You can also get a quick summary of image data: - ->>> image - -1280x800 (HxW) -Timestamp: 1699.79478134 -Field of View: 2 -Channel: GFP -Z-Level: 1 +>>> len(image_set) +7 ``` ### Bug Reports and Features diff --git a/nd2reader/__init__.py b/nd2reader/__init__.py index 979f5ec..2337034 100644 --- a/nd2reader/__init__.py +++ b/nd2reader/__init__.py @@ -32,7 +32,7 @@ class Nd2(Nd2Parser): :rtype: int """ - return self._total_images_per_channel * self._channel_count + return self._total_images_per_channel * len(self.channels) def __getitem__(self, item): """ @@ -128,8 +128,7 @@ class Nd2(Nd2Parser): 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 - using this method unless you're very familiar with the structure of ND2 files. If you have a use case that - cannot be met by the `__iter__` or `image_sets` methods above, please create an issue on Github. + using this method unless you're very familiar with the structure of ND2 files. :param time_index: the frame number :type time_index: int