Skip to content

Commit

Permalink
Support updated Images, ImageReferences, IndexSeries types (#1483)
Browse files Browse the repository at this point in the history
  • Loading branch information
rly authored May 24, 2022
1 parent e05b553 commit 728e0ac
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 22 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
change, the impact user codes should be minimal as this change primarily adds functionality while the overall
behavior of the API is largely consistent with existing behavior. @oruebel, @rly (#1390)

# Enhancements
- Added support for NWB 2.5.0.
- Added support for updated ``IndexSeries`` type, new ``order_of_images`` field in ``Images``, and new neurodata_type
``ImageReferences``. @rly (#1483)

### Documentation and tutorial enhancements:
- Added tutorial on annotating data via ``TimeIntervals``. @oruebel (#1390)
- Add copy button to code blocks @weiglszonja (#1460)
Expand Down
44 changes: 38 additions & 6 deletions src/pynwb/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,23 +105,29 @@ class TimeSeries(NWBDataInterface):
DEFAULT_DATA = np.ndarray(shape=(0, ), dtype=np.uint8)
DEFAULT_UNIT = 'unknown'

DEFAULT_RESOLUTION = -1.0
DEFAULT_CONVERSION = 1.0
DEFAULT_OFFSET = 0.0

@docval({'name': 'name', 'type': str, 'doc': 'The name of this TimeSeries dataset'}, # required
{'name': 'data', 'type': ('array_data', 'data', 'TimeSeries'),
'doc': ('The data values. The first dimension must be time. '
'Can also store binary data, e.g., image frames')},
{'name': 'unit', 'type': str, 'doc': 'The base unit of measurement (should be SI unit)'},
{'name': 'resolution', 'type': 'float',
'doc': 'The smallest meaningful difference (in specified unit) between values in data', 'default': -1.0},
'doc': 'The smallest meaningful difference (in specified unit) between values in data',
'default': DEFAULT_RESOLUTION},
{'name': 'conversion', 'type': 'float',
'doc': 'Scalar to multiply each element in data to convert it to the specified unit', 'default': 1.0},
'doc': 'Scalar to multiply each element in data to convert it to the specified unit',
'default': DEFAULT_CONVERSION},
{
'name': 'offset',
'type': 'float',
'doc': (
"Scalar to add to each element in the data scaled by 'conversion' to finish converting it to the "
"specified unit."
),
'default': 0.0
'default': DEFAULT_OFFSET
},
{'name': 'timestamps', 'type': ('array_data', 'data', 'TimeSeries'), 'shape': (None,),
'doc': 'Timestamps for samples stored in data', 'default': None},
Expand Down Expand Up @@ -295,10 +301,33 @@ def __init__(self, **kwargs):
self.description = kwargs['description']


@register_class('ImageReferences', CORE_NAMESPACE)
class ImageReferences(NWBData):
"""
Ordered dataset of references to Image objects.
"""
__nwbfields__ = ('data', )

@docval({'name': 'name', 'type': str, 'doc': 'The name of this ImageReferences object.'},
{'name': 'data', 'type': 'array_data', 'doc': 'The images in order.'},)
def __init__(self, **kwargs):
# NOTE we do not use the docval shape validator here because it will recognize a list of P MxN images as
# having shape (P, M, N)
# check type and dimensionality
for image in kwargs['data']:
assert isinstance(image, Image), "Images used in ImageReferences must have type Image, not %s" % type(image)
super().__init__(**kwargs)


@register_class('Images', CORE_NAMESPACE)
class Images(MultiContainerInterface):
"""An collection of images with an optional way to specify the order of the images
using the "order_of_images" dataset. An order must be specified if the images are
referenced by index, e.g., from an IndexSeries.
"""

__nwbfields__ = ('description',)
__nwbfields__ = ('description',
{'name': 'order_of_images', 'child': True, 'required_name': 'order_of_images'})

__clsconf__ = {
'attr': 'images',
Expand All @@ -310,12 +339,15 @@ class Images(MultiContainerInterface):

@docval({'name': 'name', 'type': str, 'doc': 'The name of this set of images'},
{'name': 'images', 'type': 'array_data', 'doc': 'image objects', 'default': None},
{'name': 'description', 'type': str, 'doc': 'description of images', 'default': 'no description'})
{'name': 'description', 'type': str, 'doc': 'description of images', 'default': 'no description'},
{'name': 'order_of_images', 'type': 'ImageReferences',
'doc': 'Ordered dataset of references to Image objects stored in the parent group.', 'default': None},)
def __init__(self, **kwargs):
name, description, images = popargs('name', 'description', 'images', kwargs)
name, description, images, order_of_images = popargs('name', 'description', 'images', 'order_of_images', kwargs)
super(Images, self).__init__(name, **kwargs)
self.description = description
self.images = images
self.order_of_images = order_of_images


class TimeSeriesReference(NamedTuple):
Expand Down
29 changes: 26 additions & 3 deletions src/pynwb/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from hdmf.utils import docval, getargs, popargs, call_docval_func, get_docval

from . import register_class, CORE_NAMESPACE
from .base import TimeSeries, Image
from .base import TimeSeries, Image, Images
from .device import Device


Expand Down Expand Up @@ -115,13 +115,36 @@ class IndexSeries(TimeSeries):
'doc': ('The data values. Must be 1D, where the first dimension must be time (frame)')},
*get_docval(TimeSeries.__init__, 'unit'), # required
{'name': 'indexed_timeseries', 'type': TimeSeries, # required
'doc': 'HDF5 link to TimeSeries containing images that are indexed.'},
'doc': 'Link to TimeSeries containing images that are indexed.', 'default': None},
{'name': 'indexed_images', 'type': Images, # required
'doc': ("Link to Images object containing an ordered set of images that are indexed. The Images object "
"must contain a 'ordered_images' dataset specifying the order of the images in the Images type."),
'default': None},
*get_docval(TimeSeries.__init__, 'resolution', 'conversion', 'timestamps', 'starting_time', 'rate',
'comments', 'description', 'control', 'control_description', 'offset'))
def __init__(self, **kwargs):
indexed_timeseries = popargs('indexed_timeseries', kwargs)
indexed_timeseries, indexed_images = popargs('indexed_timeseries', 'indexed_images', kwargs)
if kwargs['unit'] and kwargs['unit'] != 'N/A':
msg = ("The 'unit' field of IndexSeries is fixed to the value 'N/A' starting in NWB 2.5. Passing "
"a different value for 'unit' will raise an error in PyNWB 3.0.")
warnings.warn(msg, PendingDeprecationWarning)
if not indexed_timeseries and not indexed_images:
msg = "Either indexed_timeseries or indexed_images must be provided when creating an IndexSeries."
raise ValueError(msg)
if indexed_timeseries:
msg = ("The indexed_timeseries field of IndexSeries is discouraged and will be deprecated in "
"a future version of NWB. Use the indexed_images field instead.")
warnings.warn(msg, PendingDeprecationWarning)
kwargs['unit'] = 'N/A' # fixed value starting in NWB 2.5
super(IndexSeries, self).__init__(**kwargs)
self.indexed_timeseries = indexed_timeseries
self.indexed_images = indexed_images
if kwargs['conversion'] and kwargs['conversion'] != self.DEFAULT_CONVERSION:
warnings.warn("The conversion attribute is not used by IndexSeries.")
if kwargs['resolution'] and kwargs['resolution'] != self.DEFAULT_RESOLUTION:
warnings.warn("The resolution attribute is not used by IndexSeries.")
if kwargs['offset'] and kwargs['offset'] != self.DEFAULT_OFFSET:
warnings.warn("The offset attribute is not used by IndexSeries.")


@register_class('ImageMaskSeries', CORE_NAMESPACE)
Expand Down
6 changes: 3 additions & 3 deletions src/pynwb/io/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ class NWBDataMap(NWBBaseTypeMapper):
def carg_name(self, builder, manager):
return builder.name

@ObjectMapper.constructor_arg('data')
def carg_data(self, builder, manager):
return builder.data
# @ObjectMapper.constructor_arg('data')
# def carg_data(self, builder, manager):
# return builder.data


@register_map(ScratchData)
Expand Down
1 change: 1 addition & 0 deletions src/pynwb/io/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def __init__(self, spec):
self.unmap(stimulus_spec.get_group('templates'))
self.map_spec('stimulus', stimulus_spec.get_group('presentation').get_neurodata_type('TimeSeries'))
self.map_spec('stimulus_template', stimulus_spec.get_group('templates').get_neurodata_type('TimeSeries'))
self.map_spec('stimulus_template', stimulus_spec.get_group('templates').get_neurodata_type('Images'))

intervals_spec = self.spec.get_group('intervals')
self.unmap(intervals_spec)
Expand Down
13 changes: 13 additions & 0 deletions tests/integration/hdf5/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from dateutil.tz import tzlocal

from pynwb import TimeSeries, NWBFile, NWBHDF5IO
from pynwb.base import Images, Image, ImageReferences
from pynwb.testing import AcquisitionH5IOMixin, TestCase, remove_test_file


Expand Down Expand Up @@ -44,3 +45,15 @@ def test_timestamps_linking(self):
tsa = nwbfile.acquisition['a']
tsb = nwbfile.acquisition['b']
self.assertIs(tsa.timestamps, tsb.timestamps)


class TestImagesIO(AcquisitionH5IOMixin, TestCase):

def setUpContainer(self):
""" Return the test Images to read/write """
image1 = Image(name='test_image', data=np.ones((10, 10)))
image2 = Image(name='test_image2', data=np.ones((10, 10)))
image_references = ImageReferences(name='order_of_images', data=[image2, image1])
images = Images(name='images_name', images=[image1, image2], order_of_images=image_references)

return images
11 changes: 8 additions & 3 deletions tests/unit/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import numpy as np

from pynwb.base import ProcessingModule, TimeSeries, Images, Image, TimeSeriesReferenceVectorData, TimeSeriesReference
from pynwb.base import (ProcessingModule, TimeSeries, Images, Image, TimeSeriesReferenceVectorData,
TimeSeriesReference, ImageReferences)
from pynwb.testing import TestCase
from hdmf.data_utils import DataChunkIterator
from hdmf.backends.hdf5 import H5DataIO
Expand Down Expand Up @@ -246,9 +247,13 @@ def test_image(self):
class TestImages(TestCase):

def test_images(self):
image = Image(name='test_image', data=np.ones((10, 10)))
image1 = Image(name='test_image', data=np.ones((10, 10)))
image2 = Image(name='test_image2', data=np.ones((10, 10)))
Images(name='images_name', images=[image, image2])
image_references = ImageReferences(name='order_of_images', data=[image2, image1])
images = Images(name='images_name', images=[image1, image2], order_of_images=image_references)

self.assertIs(images.order_of_images[0], image2)
self.assertIs(images.order_of_images[1], image1)


class TestTimeSeriesReferenceVectorData(TestCase):
Expand Down
50 changes: 44 additions & 6 deletions tests/unit/test_image.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import numpy as np

from pynwb import TimeSeries
from pynwb.base import Image, Images, ImageReferences
from pynwb.device import Device
from pynwb.image import ImageSeries, IndexSeries, ImageMaskSeries, OpticalSeries, GrayscaleImage, RGBImage, RGBAImage
from pynwb.testing import TestCase
Expand Down Expand Up @@ -76,21 +77,58 @@ def test_external_file_no_unit(self):
class IndexSeriesConstructor(TestCase):

def test_init(self):
image1 = Image(name='test_image', data=np.ones((10, 10)))
image2 = Image(name='test_image2', data=np.ones((10, 10)))
image_references = ImageReferences(name='order_of_images', data=[image2, image1])
images = Images(name='images_name', images=[image1, image2], order_of_images=image_references)

iS = IndexSeries(
name='test_iS',
data=[1, 2, 3],
unit='N/A',
indexed_images=images,
timestamps=[0.1, 0.2, 0.3]
)
self.assertEqual(iS.name, 'test_iS')
self.assertEqual(iS.unit, 'N/A')
self.assertIs(iS.indexed_images, images)

def test_init_bad_unit(self):
ts = TimeSeries(
name='test_ts',
data=[1, 2, 3],
unit='unit',
timestamps=[0.1, 0.2, 0.3]
)
iS = IndexSeries(
name='test_iS',
msg = ("The 'unit' field of IndexSeries is fixed to the value 'N/A' starting in NWB 2.5. Passing "
"a different value for 'unit' will raise an error in PyNWB 3.0.")
with self.assertWarnsWith(PendingDeprecationWarning, msg):
iS = IndexSeries(
name='test_iS',
data=[1, 2, 3],
unit='na',
indexed_timeseries=ts,
timestamps=[0.1, 0.2, 0.3]
)
self.assertEqual(iS.unit, 'N/A')

def test_init_indexed_ts(self):
ts = TimeSeries(
name='test_ts',
data=[1, 2, 3],
unit='N/A',
indexed_timeseries=ts,
unit='unit',
timestamps=[0.1, 0.2, 0.3]
)
self.assertEqual(iS.name, 'test_iS')
self.assertEqual(iS.unit, 'N/A')
msg = ("The indexed_timeseries field of IndexSeries is discouraged and will be deprecated in "
"a future version of NWB. Use the indexed_images field instead.")
with self.assertWarnsWith(PendingDeprecationWarning, msg):
iS = IndexSeries(
name='test_iS',
data=[1, 2, 3],
unit='N/A',
indexed_timeseries=ts,
timestamps=[0.1, 0.2, 0.3]
)
self.assertIs(iS.indexed_timeseries, ts)


Expand Down

0 comments on commit 728e0ac

Please sign in to comment.