Browse Source

Merge pull request #130 from jimrybarski/129-rename-filter

resolves #129: renamed filter() to select()
zolfa-add_slices_loading
Jim Rybarski 9 years ago
parent
commit
4ccf77551d
5 changed files with 90 additions and 64 deletions
  1. +6
    -2
      Dockerfile
  2. +9
    -0
      README.md
  3. +6
    -6
      functional_tests/FYLM141111001.py
  4. +68
    -55
      nd2reader/interface.py
  5. +1
    -1
      tests/model/image.py

+ 6
- 2
Dockerfile View File

@ -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

+ 9
- 0
README.md View File

@ -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


+ 6
- 6
functional_tests/FYLM141111001.py View File

@ -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))


+ 68
- 55
nd2reader/interface.py View File

@ -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(["<ND2 %s>" % 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):
"""


+ 1
- 1
tests/model/image.py View File

@ -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)


Loading…
Cancel
Save