Skip to content

Commit

Permalink
Merge pull request #1 from teogale/imagesequence
Browse files Browse the repository at this point in the history
WIP : support for image sequence data
  • Loading branch information
apdavison authored Sep 25, 2019
2 parents 3a9082e + 07ffa4c commit 04b56fa
Show file tree
Hide file tree
Showing 27 changed files with 1,431 additions and 33 deletions.
1 change: 1 addition & 0 deletions .circleci/requirements_testing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ ipython
https://github.com/nsdf/nsdf/archive/0.1.tar.gz
coverage
coveralls
pillow
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ install:
- pip install -r requirements.txt
- pip install coveralls
- pip install .
- pip install pillow
# command to run tests, e.g. python setup.py test
script:
nosetests --with-coverage --cover-package=neo
2 changes: 1 addition & 1 deletion doc/source/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ API Reference
.. testsetup:: *

from neo import SpikeTrain
import quantities as pq
import quantities as pq
2 changes: 1 addition & 1 deletion doc/source/core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ associated metadata (units, sampling frequency, etc.).
* :py:class:`SpikeTrain`: A set of action potentials (spikes) emitted by the same unit in a period of time (with optional waveforms).
* :py:class:`Event`: An array of time points representing one or more events in the data.
* :py:class:`Epoch`: An array of time intervals representing one or more periods of time in the data.

* :py:class:`ImageSequence`: A three dimension array representing multiple images.

Container objects
-----------------
Expand Down
31 changes: 31 additions & 0 deletions examples/imageseq.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from neo.core import ImageSequence
from neo.core import RectangularRegionOfInterest, CircularRegionOfInterest, PolygonRegionOfInterest
import matplotlib.pyplot as plt
import quantities as pq

import random

# generate data

l = []
for frame in range(50):
l.append([])
for y in range(100):
l[frame].append([])
for x in range(100):
l[frame][y].append(random.randint(0, 50))

image_seq = ImageSequence(l, sampling_rate=500 * pq.Hz, spatial_scale='m', units='V')

result = image_seq.signal_from_region(CircularRegionOfInterest(50, 50, 25),
CircularRegionOfInterest(10, 10, 5),
PolygonRegionOfInterest((50, 25), (50, 45), (14, 65),
(90, 80)))

for i in range(len(result)):
plt.figure()
plt.plot(result[i].times, result[i])
plt.xlabel("seconde")
plt.ylabel("valeur")

plt.show()
8 changes: 7 additions & 1 deletion neo/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
.. autoclass:: Epoch
.. autoclass:: SpikeTrain
.. autoclass:: ImageSequence
"""

Expand All @@ -41,10 +42,15 @@

from neo.core.spiketrain import SpikeTrain

from neo.core.imagesequence import ImageSequence
from neo.core.regionofinterest import RectangularRegionOfInterest, CircularRegionOfInterest, PolygonRegionOfInterest

# Block should always be first in this list
objectlist = [Block, Segment, ChannelIndex,
AnalogSignal, IrregularlySampledSignal,
Event, Epoch, Unit, SpikeTrain]
Event, Epoch, Unit, SpikeTrain, ImageSequence,
RectangularRegionOfInterest, CircularRegionOfInterest,
PolygonRegionOfInterest]

objectnames = [ob.__name__ for ob in objectlist]
class_by_name = dict(zip(objectnames, objectlist))
1 change: 1 addition & 0 deletions neo/core/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def __init__(self, name=None, description=None, file_origin=None,
self.file_datetime = file_datetime
self.rec_datetime = rec_datetime
self.index = index
self.regionsofinterest = []

@property
def data_children_recur(self):
Expand Down
1 change: 1 addition & 0 deletions neo/core/dataobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def _normalize_array_annotations(value, length):
for key in value.keys():
if isinstance(value[key], dict):
raise ValueError("Nested dicts are not allowed as array annotations")

value[key] = _normalize_array_annotations(value[key], length)

elif value is None:
Expand Down
195 changes: 195 additions & 0 deletions neo/core/imagesequence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# -*- coding: utf-8 -*-
"""
This module implements :class:`ImageSequence`, a 3D array.
:class:`ImageSequence` inherits from :class:`basesignal.BaseSignal` which
derives from :class:`BaseNeo`, and from :class:`quantites.Quantity`which
in turn inherits from :class:`numpy.array`.
Inheritance from :class:`numpy.array` is explained here:
http://docs.scipy.org/doc/numpy/user/basics.subclassing.html
In brief:
* Initialization of a new object from constructor happens in :meth:`__new__`.
This is where user-specified attributes are set.
* :meth:`__array_finalize__` is called for all new objects, including those
created by slicing. This is where attributes are copied over from
the old object.
"""

from neo.core.analogsignal import AnalogSignal, _get_sampling_rate
import quantities as pq
import numpy as np
from neo.core.baseneo import BaseNeo
from neo.core.basesignal import BaseSignal
from neo.core.dataobject import DataObject


class ImageSequence(BaseSignal):
"""
Array of three dimension organize as [frame][row][column].
Inherits from :class:`quantities.Quantity`, which in turn inherits from
:class:`numpy.ndarray`.
*usage*::
>>> from neo.core import ImageSequence
>>> import quantities as pq
>>>
>>> img_sequence_array = [[[column for column in range(20)]for row in range(20)]
... for frame in range(10)]
>>> image_sequence = ImageSequence(img_sequence_array, units='V',
... sampling_rate=1*pq.Hz, spatial_scale=1*pq.micrometer)
>>> image_sequence
ImageSequence 10 frame with 20 px of height and 20 px of width; units V; datatype int64
sampling rate: 1.0
spatial_scale: 1.0
>>> image_sequence.spatial_scale
array(1.) * um
*Required attributes/properties*:
:image_data: (three dimension numpy array, or a three dimension list)
The data itself
:units: (quantity units)
:sampling_rate: *or* **sampling_period** (quantity scalar) Number of
samples per unit time or
interval beween to samples.
If both are specified, they are
checked for consistency.
:spatial_scale: (quantity scalar) size for a pixel.
*Recommended attributes/properties*:
:name: (str) A label for the dataset.
:description: (str) Text description.
:file_origin: (str) Filesystem path or URL of the original data file.
*Optional attributes/properties*:
:dtype: (numpy dtype or str) Override the dtype of the signal array.
:copy: (bool) True by default.
Note: Any other additional arguments are assumed to be user-specific
metadata and stored in :attr:`annotations`.
*Properties available on this object*:
:sampling_rate: (quantity scalar) Number of samples per unit time.
(1/:attr:`sampling_period`)
:sampling_period: (quantity scalar) Interval between two samples.
(1/:attr:`quantity scalar`)
:spatial_scales: size of a pixel
"""
_single_parent_objects = ('Segment',)
_single_parent_attrs = ('segment',)
_quantity_attr = 'image_data'
_necessary_attrs = (('image_data', pq.Quantity, 3),
('sampling_rate', pq.Quantity, 0),
('spatial_scale', pq.Quantity, 0))
_recommended_attrs = BaseNeo._recommended_attrs

def __new__(cls, image_data, units=None, dtype=None, copy=True, spatial_scale=None, sampling_period=None,
sampling_rate=None, name=None, description=None, file_origin=None,
**annotations):
"""
Constructs new :class:`ImageSequence` from data.
This is called whenever a new class:`ImageSequence` is created from
the constructor, but not when slicing.
__array_finalize__ is called on the new object.
"""
if spatial_scale is None:
raise ValueError('spatial_scale is required')

image_data = np.stack(image_data)
if len(image_data.shape) != 3:
raise ValueError('list doesn\'t have the good number of dimension')

obj = pq.Quantity(image_data, units=units, dtype=dtype, copy=copy).view(cls)
obj.segment = None
# function from analogsignal.py in neo/core directory
obj.sampling_rate = _get_sampling_rate(sampling_rate, sampling_period)
obj.spatial_scale = spatial_scale

return obj

def __init__(self, image_data, units=None, dtype=None, copy=True, spatial_scale=None, sampling_period=None,
sampling_rate=None, name=None, description=None, file_origin=None,
**annotations):
'''
Initializes a newly constructed :class:`ImageSequence` instance.
'''
DataObject.__init__(self, name=name, file_origin=file_origin, description=description,
**annotations)

def __array_finalize__spec(self, obj):

self.sampling_rate = getattr(obj, 'sampling_rate', None)
self.spatial_scale = getattr(obj, 'spatial_scale', None)
self.units = getattr(obj, 'units', None)

return obj

def signal_from_region(self, *region):

"""
Method that takes 1 or multiple regionofinterest, use the method of each region
of interest to get the list of pixel to average.
return a list of :class:`AnalogSignal` for each regionofinterest
"""

if len(region) == 0:
raise ValueError('no region of interest have been given')

region_pixel = []
for i, b in enumerate(region):
r = region[i].return_list_pixel()
if not r:
raise ValueError('region '+str(i)+'is empty')
else:
region_pixel.append(r)
analogsignal_list = []
for i in region_pixel:
data = []
for frame in range(len(self)):
picture_data = []
for v in i:
picture_data.append(self.view(pq.Quantity)[frame][v[0]][v[1]])
average = picture_data[0]
for b in range(1, len(picture_data)):
average += picture_data[b]
data.append((average * 1.0) / len(i))
analogsignal_list.append(AnalogSignal(data, units=self.units, sampling_rate=self.sampling_rate))

return analogsignal_list

def _repr_pretty_(self, pp, cycle):
'''
Handle pretty-printing the :class:`ImageSequence`.
'''
pp.text("{cls} {frame} frame with {width} px of width and {height} px of height; "
"units {units}; datatype {dtype} ".format(cls=self.__class__.__name__,
frame=self.shape[0],
height=self.shape[1],
width=self.shape[2],
units=self.units.dimensionality.string,
dtype=self.dtype))

def _pp(line):
pp.breakable()
with pp.group(indent=1):
pp.text(line)

for line in ["sampling rate: {0}".format(self.sampling_rate),
"spatial_scale: {0}".format(self.spatial_scale)]:
_pp(line)

def _check_consistency(self, other):
'''
Check if the attributes of another :class:`ImageSequence`
are compatible with this one.
'''
if isinstance(other, ImageSequence):
for attr in ("sampling_rate", "spatial_scale"):
if getattr(self, attr) != getattr(other, attr):
raise ValueError("Inconsistent values of %s" % attr)
Loading

0 comments on commit 04b56fa

Please sign in to comment.