Browse Source

Merge pull request #58 from jimrybarski/dev

Dev
feature/load_slices
Jim Rybarski 10 years ago
parent
commit
d615eea5cc
10 changed files with 536 additions and 215 deletions
  1. +16
    -0
      CHANGELOG.md
  2. +13
    -6
      Dockerfile
  3. +10
    -3
      Makefile
  4. +109
    -48
      README.md
  5. +73
    -37
      nd2reader/__init__.py
  6. +120
    -118
      nd2reader/parser.py
  7. +1
    -1
      requirements.txt
  8. +6
    -2
      setup.py
  9. +5
    -0
      tests.py
  10. +183
    -0
      tests/__init__.py

+ 16
- 0
CHANGELOG.md View File

@ -0,0 +1,16 @@
## [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!

+ 13
- 6
Dockerfile View File

@ -1,13 +1,20 @@
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
RUN apt-get update && apt-get install -y \
python-numpy
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libatlas3-base \
liblapack-dev \
libblas-dev \
python \
python3 \
python-dev \
python3-dev \
python-pip \
python3-pip
COPY . /opt/nd2reader COPY . /opt/nd2reader
WORKDIR /opt/nd2reader WORKDIR /opt/nd2reader
RUN python setup.py install RUN python setup.py install
WORKDIR /var/nd2s
CMD /usr/bin/python2.7
RUN python3 setup.py install

+ 10
- 3
Makefile View File

@ -1,7 +1,14 @@
.PHONY: build shell
.PHONY: build py2shell py3shell test
build: build:
docker build -t jimrybarski/nd2reader . docker build -t jimrybarski/nd2reader .
shell:
docker run --rm -v ~/Documents/nd2s:/var/nd2s -it jimrybarski/nd2reader
py2shell:
docker run --rm -v ~/Documents/nd2s:/var/nd2s -it jimrybarski/nd2reader python2.7
py3shell:
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

+ 109
- 48
README.md View File

@ -10,16 +10,19 @@
### Installation ### Installation
Just use pip (`numpy` is required):
Dependencies will automatically be installed if you don't have them. That said, for optimal performance, you should
install the following packages before installing nd2reader:
`pip install numpy nd2reader`
#### Ubuntu
`apt-get install python-numpy python-six` (Python 2.x)
`apt-get install python3-numpy python3-six` (Python 3.x)
If you want to install via git, clone the repo and run:
#### Other operating systems
These have not been tested yet.
```
pip install numpy
python setup.py install
```
nd2reader is compatible with both Python 2.x and 3.x. I recommend installing using pip:
`pip install nd2reader` (Python 2.x) or `pip3 install nd2reader` (Python 3.x)
### ND2s ### ND2s
@ -37,9 +40,58 @@ Fields of View: 8
Z-Levels: 3 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
Every method returns 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
<ND2 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 ```python
import nd2reader import nd2reader
@ -48,10 +100,42 @@ for image in nd2:
do_something(image.data) do_something(image.data)
``` ```
You can also get an image directly by indexing. Here, we look at the 38th image:
```python
>>> nd2[37]
<ND2 Image>
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 ### Image Sets
If you have complicated hierarchical data, it may be easier to use image sets, which groups images together if they 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 ```python
import nd2reader import nd2reader
@ -59,55 +143,32 @@ nd2 = nd2reader.Nd2("/path/to/my_complicated_images.nd2")
for image_set in nd2.image_sets: for image_set in nd2.image_sets:
# you can select images by channel # you can select images by channel
gfp_image = image_set.get("GFP") 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 # 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) 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
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.
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`:
### 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 ```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
<ND2 Image>
1280x800 (HxW)
Timestamp: 1699.79478134
Field of View: 2
Channel: GFP
Z-Level: 1
>>> len(image_set)
7
``` ```
### Protips
nd2reader is about 14 times faster under Python 3.4 compared to Python 2.7. If you know why, please get in touch!
### Bug Reports and Features ### Bug Reports and Features
If this fails to work exactly as expected, please open a Github issue. If you get an unhandled exception, please If this fails to work exactly as expected, please open a Github issue. If you get an unhandled exception, please


+ 73
- 37
nd2reader/__init__.py View File

@ -16,12 +16,12 @@ 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" % self._time_index_count,
"Channels: %s" % ", ".join(["'%s'" % str(channel) for channel in self._channels]),
"Fields of View: %s" % self._field_of_view_count,
"Z-Levels: %s" % self._z_level_count
"Image cycles: %s" % len(self.time_indexes),
"Channels: %s" % ", ".join(["'%s'" % str(channel) for channel in self.channels]),
"Fields of View: %s" % len(self.fields_of_view),
"Z-Levels: %s" % len(self.z_levels)
]) ])
def __len__(self): def __len__(self):
@ -32,40 +32,59 @@ class Nd2(Nd2Parser):
:rtype: int :rtype: int
""" """
return self._image_count * self._channel_count
return self._total_images_per_channel * len(self.channels)
@property
def height(self):
def __getitem__(self, item):
""" """
:return: height of each image, in pixels
:rtype: int
Allows slicing ND2s.
"""
return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiHeight')]
>>> nd2 = Nd2("my_images.nd2")
>>> image = nd2[16] # gets 17th frame
>>> for image in nd2[100:200]: # iterate over the 100th to 200th images
>>> do_something(image.data)
>>> for image in nd2[::-1]: # iterate backwards
>>> do_something(image.data)
>>> for image in nd2[37:422:17]: # do something super weird if you really want to
>>> do_something(image.data)
@property
def width(self):
"""
:return: width of each image, in pixels
:rtype: int
:type item: int or slice
:rtype: nd2reader.model.Image() or generator
""" """
return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiWidth')]
def __iter__(self):
if isinstance(item, int):
try:
channel_offset = item % len(self.channels)
fov = self._calculate_field_of_view(item)
channel = self._calculate_channel(item)
z_level = self._calculate_z_level(item)
timestamp, raw_image_data = self._get_raw_image_data(item, channel_offset)
image = Image(timestamp, raw_image_data, fov, channel, z_level, self.height, self.width)
except (TypeError, ValueError):
return None
except KeyError:
raise IndexError("Invalid frame number.")
else:
return image
elif isinstance(item, slice):
return self._slice(item.start, item.stop, item.step)
raise IndexError
def _slice(self, start, stop, step):
""" """
Iterates over every image, in the order they were taken.
Allows for iteration over a selection of the entire dataset.
:return: model.Image()
:type start: int
:type stop: int
:type step: int
:rtype: nd2reader.model.Image() or None
""" """
for i in range(self._image_count):
for fov in range(self._field_of_view_count):
for z_level in range(self._z_level_count):
for channel_name in self._channels:
image = self.get_image(i, fov, channel_name, z_level)
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]
@property @property
def image_sets(self): def image_sets(self):
@ -78,21 +97,38 @@ class Nd2(Nd2Parser):
:return: model.ImageSet() :return: model.ImageSet()
""" """
for time_index in range(self._time_index_count):
for time_index in self.time_indexes:
image_set = ImageSet() image_set = ImageSet()
for fov in range(self._field_of_view_count):
for channel_name in self._channels:
for z_level in range(self._z_level_count):
for fov in self.fields_of_view:
for channel_name in self.channels:
for z_level in self.z_levels:
image = self.get_image(time_index, fov, channel_name, z_level) image = self.get_image(time_index, fov, channel_name, z_level)
if image is not None: if image is not None:
image_set.add(image) image_set.add(image)
yield image_set yield image_set
@property
def height(self):
"""
:return: height of each image, in pixels
:rtype: int
"""
return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiHeight')]
@property
def width(self):
"""
:return: width of each image, in pixels
:rtype: int
"""
return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiWidth')]
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
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 :param time_index: the frame number
:type time_index: int :type time_index: int
@ -105,9 +141,9 @@ class Nd2(Nd2Parser):
:rtype: nd2reader.model.Image() or None :rtype: nd2reader.model.Image() or None
""" """
image_set_number = self._calculate_image_group_number(time_index, field_of_view, z_level)
image_group_number = self._calculate_image_group_number(time_index, field_of_view, z_level)
try: try:
timestamp, raw_image_data = self._get_raw_image_data(image_set_number, self._channel_offset[channel_name])
timestamp, raw_image_data = self._get_raw_image_data(image_group_number, self._channel_offset[channel_name])
image = Image(timestamp, raw_image_data, field_of_view, channel_name, z_level, self.height, self.width) image = Image(timestamp, raw_image_data, field_of_view, channel_name, z_level, self.height, self.width)
except TypeError: except TypeError:
return None return None


+ 120
- 118
nd2reader/parser.py View File

@ -19,15 +19,124 @@ 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._channel_count = None
self._chunk_map_start_location = None self._chunk_map_start_location = None
self._cursor_position = 0 self._cursor_position = 0
self._dimension_text = None self._dimension_text = None
self._fields_of_view = None
self._label_map = {} self._label_map = {}
self.metadata = {} self.metadata = {}
self._read_map() self._read_map()
self._time_indexes = None
self._parse_metadata() self._parse_metadata()
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):
@ -58,7 +167,7 @@ class Nd2Parser(object):
# The images for the various channels are interleaved within the same array. For example, the second image # The images for the various channels are interleaved within the same array. For example, the second image
# of a four image group will be composed of bytes 2, 6, 10, etc. If you understand why someone would design # of a four image group will be composed of bytes 2, 6, 10, etc. If you understand why someone would design
# a data structure that way, please send the author of this library a message. # a data structure that way, please send the author of this library a message.
image_data = image_group_data[image_data_start::self._channel_count]
image_data = image_group_data[image_data_start::len(self.channels)]
# Skip images that are all zeros! This is important, since NIS Elements creates blank "gap" images if you # Skip images that are all zeros! This is important, since NIS Elements creates blank "gap" images if you
# don't have the same number of images each cycle. We discovered this because we only took GFP images every # don't have the same number of images each cycle. We discovered this because we only took GFP images every
# other cycle to reduce phototoxicity, but NIS Elements still allocated memory as if we were going to take # other cycle to reduce phototoxicity, but NIS Elements still allocated memory as if we were going to take
@ -90,33 +199,8 @@ class Nd2Parser(object):
break break
else: else:
raise ValueError("Could not parse metadata dimensions!") raise ValueError("Could not parse metadata dimensions!")
if six.PY3:
return self._dimension_text.decode("utf8")
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
"""
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
yield chan[six.b('sDescription')].decode("utf8")
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.
@ -128,7 +212,7 @@ class Nd2Parser(object):
:rtype: int :rtype: int
""" """
return time_index * self._field_of_view_count * self._z_level_count + (fov * self._z_level_count + z_level)
return time_index * len(self.fields_of_view) * len(self.z_levels) + (fov * len(self.z_levels) + z_level)
@property @property
def _channel_offset(self): def _channel_offset(self):
@ -144,103 +228,21 @@ 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.")
@property
def _channel_count(self):
"""
The number of different channels used, including bright field.
:rtype: int
"""
pattern = r""".*?λ\((\d+)\).*?"""
try:
count = int(re.match(pattern, self._dimensions).group(1))
except AttributeError as e:
return 1
else:
return count
@property
def _field_of_view_count(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
"""
pattern = r""".*?XY\((\d+)\).*?"""
try:
count = int(re.match(pattern, self._dimensions).group(1))
except AttributeError:
return 1
else:
return count
@property
def _time_index_count(self):
"""
The number of cycles.
:rtype: int
"""
pattern = r""".*?T'\((\d+)\).*?"""
try:
count = int(re.match(pattern, self._dimensions).group(1))
except AttributeError:
return 1
else:
return count
@property
def _z_level_count(self):
"""
The number of different levels in the Z-plane.
:rtype: int
"""
pattern = r""".*?Z\((\d+)\).*?"""
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))
except AttributeError: except AttributeError:
return 1
return [0]
except TypeError:
count = int(re.match(pattern, self._dimensions.decode("utf8")).group(1))
return list(range(count))
else: else:
return count
return list(range(count))
@property @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
@ -376,4 +378,4 @@ class Nd2Parser(object):
# We've encountered this key before so we're guaranteed to be dealing with a list. Thus we append # We've encountered this key before so we're guaranteed to be dealing with a list. Thus we append
# the value to the already-existing list. # the value to the already-existing list.
metadata[name].append(value) metadata[name].append(value)
return metadata
return metadata

+ 1
- 1
requirements.txt View File

@ -1,2 +1,2 @@
numpy>=1.9.2 numpy>=1.9.2
six
six>=1.4

+ 6
- 2
setup.py View File

@ -1,10 +1,14 @@
from distutils.core import setup
from setuptools import setup
VERSION = "1.0.1"
VERSION = "1.1.0"
setup( setup(
name="nd2reader", name="nd2reader",
packages=['nd2reader', 'nd2reader.model'], packages=['nd2reader', 'nd2reader.model'],
install_requires=[
'numpy>=1.6.2, <2.0',
'six>=1.4, <2.0'
],
version=VERSION, version=VERSION,
description='A tool for reading ND2 files produced by NIS Elements', description='A tool for reading ND2 files produced by NIS Elements',
author='Jim Rybarski', author='Jim Rybarski',


+ 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