Skip to content

Commit

Permalink
Merge pull request #1313 from apdavison/fix-regionsofinterest
Browse files Browse the repository at this point in the history
make the handling of RegionOfInterest subclasses consistent with ChannelView
  • Loading branch information
samuelgarcia authored Jan 26, 2024
2 parents 32c3b99 + b4f0029 commit 54d65ef
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 37 deletions.
6 changes: 5 additions & 1 deletion neo/core/baseneo.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,11 @@ def _container_name(class_name):
referenced by `block.segments`. The attribute name `segments` is
obtained by calling `_container_name_plural("Segment")`.
"""
return _reference_name(class_name) + 's'
if "RegionOfInterest" in class_name:
# this is a hack, pending a more principled way to handle this
return "regionsofinterest"
else:
return _reference_name(class_name) + 's'


class BaseNeo:
Expand Down
8 changes: 0 additions & 8 deletions neo/core/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from neo.core.container import Container, unique_objs
from neo.core.group import Group
from neo.core.objectlist import ObjectList
from neo.core.regionofinterest import RegionOfInterest
from neo.core.segment import Segment


Expand Down Expand Up @@ -91,7 +90,6 @@ def __init__(self, name=None, description=None, file_origin=None,
self.index = index
self._segments = ObjectList(Segment, parent=self)
self._groups = ObjectList(Group, parent=self)
self._regionsofinterest = ObjectList(RegionOfInterest, parent=self)

segments = property(
fget=lambda self: self._get_object_list("_segments"),
Expand All @@ -105,12 +103,6 @@ def __init__(self, name=None, description=None, file_origin=None,
doc="list of Groups contained in this block"
)

regionsofinterest = property(
fget=lambda self: self._get_object_list("_regionsofinterest"),
fset=lambda self, value: self._set_object_list("_regionsofinterest", value),
doc="list of RegionOfInterest objects contained in this block"
)

@property
def data_children_recur(self):
'''
Expand Down
11 changes: 10 additions & 1 deletion neo/core/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from neo.core.segment import Segment
from neo.core.spiketrainlist import SpikeTrainList
from neo.core.view import ChannelView
from neo.core.regionofinterest import RegionOfInterest


class Group(Container):
Expand Down Expand Up @@ -49,7 +50,8 @@ class Group(Container):
"""
_data_child_objects = (
'AnalogSignal', 'IrregularlySampledSignal', 'SpikeTrain',
'Event', 'Epoch', 'ChannelView', 'ImageSequence'
'Event', 'Epoch', 'ChannelView', 'ImageSequence', 'CircularRegionOfInterest',
'RectangularRegionOfInterest', 'PolygonRegionOfInterest'
)
_container_child_objects = ('Group',)
_parent_objects = ('Block',)
Expand All @@ -69,6 +71,7 @@ def __init__(self, objects=None, name=None, description=None, file_origin=None,
self._epochs = ObjectList(Epoch)
self._channelviews = ObjectList(ChannelView)
self._imagesequences = ObjectList(ImageSequence)
self._regionsofinterest = ObjectList(RegionOfInterest)
self._segments = ObjectList(Segment) # to remove?
self._groups = ObjectList(Group)

Expand Down Expand Up @@ -119,6 +122,12 @@ def __init__(self, objects=None, name=None, description=None, file_origin=None,
doc="list of ImageSequences contained in this group"
)

regionsofinterest = property(
fget=lambda self: self._get_object_list("_regionsofinterest"),
fset=lambda self, value: self._set_object_list("_regionsofinterest", value),
doc="list of RegionOfInterest objects contained in this group"
)

spiketrains = property(
fget=lambda self: self._get_object_list("_spiketrains"),
fset=lambda self, value: self._set_object_list("_spiketrains", value),
Expand Down
6 changes: 3 additions & 3 deletions neo/core/imagesequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class ImageSequence(BaseSignal):
)
_recommended_attrs = BaseNeo._recommended_attrs

def __new__(cls, image_data, units=None, dtype=None, copy=True, t_start=0 * pq.s,
def __new__(cls, image_data, units=pq.dimensionless, dtype=None, copy=True, t_start=0 * pq.s,
spatial_scale=None, frame_duration=None,
sampling_rate=None, name=None, description=None, file_origin=None,
**annotations):
Expand Down Expand Up @@ -127,7 +127,7 @@ def __new__(cls, image_data, units=None, dtype=None, copy=True, t_start=0 * pq.s

return obj

def __init__(self, image_data, units=None, dtype=None, copy=True, t_start=0 * pq.s,
def __init__(self, image_data, units=pq.dimensionless, dtype=None, copy=True, t_start=0 * pq.s,
spatial_scale=None, frame_duration=None,
sampling_rate=None, name=None, description=None, file_origin=None,
**annotations):
Expand All @@ -142,7 +142,7 @@ 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)
self.units = getattr(obj, "units", pq.dimensionless)
self._t_start = getattr(obj, "_t_start", 0 * pq.s)

return obj
Expand Down
36 changes: 31 additions & 5 deletions neo/core/regionofinterest.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
from math import floor, ceil

from neo.core.baseneo import BaseNeo
from neo.core.imagesequence import ImageSequence


class RegionOfInterest(BaseNeo):
"""Abstract base class"""
pass

_parent_objects = ('Group',)
_parent_attrs = ('group',)
_necessary_attrs = (
('obj', ('ImageSequence', ), 1),
)

def __init__(self, image_sequence, name=None, description=None, file_origin=None, **annotations):
super().__init__(name=name, description=description,
file_origin=file_origin, **annotations)

if not (isinstance(image_sequence, ImageSequence) or (
hasattr(image_sequence, "proxy_for") and issubclass(image_sequence.proxy_for, ImageSequence))):
raise ValueError("Can only take a RegionOfInterest of an ImageSequence")
self.image_sequence = image_sequence

def resolve(self):
"""
Return a signal from within this region of the underlying ImageSequence.
"""
return self.image_sequence.signal_from_region(self)


class CircularRegionOfInterest(RegionOfInterest):
Expand All @@ -23,8 +44,9 @@ class CircularRegionOfInterest(RegionOfInterest):
Radius of the ROI in pixels
"""

def __init__(self, x, y, radius):

def __init__(self, image_sequence, x, y, radius, name=None, description=None,
file_origin=None, **annotations):
super().__init__(image_sequence, name, description, file_origin, **annotations)
self.y = y
self.x = x
self.radius = radius
Expand Down Expand Up @@ -72,7 +94,9 @@ class RectangularRegionOfInterest(RegionOfInterest):
Height (y-direction) of the ROI in pixels
"""

def __init__(self, x, y, width, height):
def __init__(self, image_sequence, x, y, width, height, name=None, description=None,
file_origin=None, **annotations):
super().__init__(image_sequence, name, description, file_origin, **annotations)
self.x = x
self.y = y
self.width = width
Expand Down Expand Up @@ -115,7 +139,9 @@ class PolygonRegionOfInterest(RegionOfInterest):
of the vertices of the polygon
"""

def __init__(self, *vertices):
def __init__(self, image_sequence, *vertices, name=None, description=None,
file_origin=None, **annotations):
super().__init__(image_sequence, name, description, file_origin, **annotations)
self.vertices = vertices

def polygon_ray_casting(self, bounding_points, bounding_box_positions):
Expand Down
12 changes: 11 additions & 1 deletion neo/test/coretest/test_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@
from neo.core.view import ChannelView
from neo.core.group import Group
from neo.core.block import Block
from neo.core.imagesequence import ImageSequence
from neo.core.regionofinterest import CircularRegionOfInterest


class TestGroup(unittest.TestCase):

def setUp(self):
test_data = np.random.rand(100, 8) * pq.mV
channel_names = np.array(["a", "b", "c", "d", "e", "f", "g", "h"])
test_image_data = np.random.rand(640).reshape(10, 8, 8)
self.test_signal = AnalogSignal(test_data,
sampling_period=0.1 * pq.ms,
name="test signal",
Expand All @@ -34,21 +37,28 @@ def setUp(self):
description="this is a view of a test signal",
array_annotations={"something": np.array(["A", "B", "C", "D"])},
sLaTfat="fish")
self.test_image_seq = ImageSequence(test_image_data,
frame_duration=20 * pq.ms,
spatial_scale=1 * pq.um)
self.roi = CircularRegionOfInterest(self.test_image_seq, 0, 0, 3)
self.test_spiketrains = [SpikeTrain(np.arange(100.0), units="ms", t_stop=200),
SpikeTrain(np.arange(0.5, 100.5), units="ms", t_stop=200)]
self.test_segment = Segment()
self.test_segment.analogsignals.append(self.test_signal)
self.test_segment.spiketrains.extend(self.test_spiketrains)
self.test_segment.imagesequences.append(self.test_image_seq)

def test_create_group(self):
objects = [self.test_view, self.test_signal]
objects = [self.test_view, self.test_signal, self.test_image_seq, self.roi]
objects.extend(self.test_spiketrains)
group = Group(objects)

assert group.analogsignals[0] is self.test_signal
assert group.spiketrains[0] is self.test_spiketrains[0]
assert group.spiketrains[1] is self.test_spiketrains[1]
assert group.channelviews[0] is self.test_view
assert group.imagesequences[0] is self.test_image_seq
assert group.regionsofinterest[0] is self.roi
assert len(group.irregularlysampledsignals) == 0

def test_create_empty_group(self):
Expand Down
23 changes: 12 additions & 11 deletions neo/test/coretest/test_imagesequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def test_error_spatial_scale(self):

def test_units(self):
with self.assertRaises(TypeError):
ImageSequence(self.data, sampling_rate=500 * pq.Hz, spatial_scale=1 * pq.um)
ImageSequence(self.data, units=None, sampling_rate=500 * pq.Hz, spatial_scale=1 * pq.um)

def test_wrong_dimensions(self):
seq = ImageSequence(self.data, sampling_rate=500 * pq.Hz,
Expand Down Expand Up @@ -71,36 +71,37 @@ def test_t_start(self):


class TestMethodImageSequence(unittest.TestCase):
def fake_region_of_interest(self):
self.rect_ROI = RectangularRegionOfInterest(2, 2, 2, 2)
def _create_test_objects(self):
self.data = []
for frame in range(25):
self.data.append([])
for y in range(5):
self.data[frame].append([])
for x in range(5):
self.data[frame][y].append(x)

def test_signal_from_region(self):
self.fake_region_of_interest()
seq = ImageSequence(
self.seq = ImageSequence(
self.data,
units="V",
sampling_rate=500 * pq.Hz,
t_start=250 * pq.ms,
spatial_scale=1 * pq.um,
)
signals = seq.signal_from_region(self.rect_ROI)
self.rect_ROI = RectangularRegionOfInterest(self.seq, 2, 2, 2, 2)

def test_signal_from_region(self):
self._create_test_objects()
signals = self.seq.signal_from_region(self.rect_ROI)
self.assertIsInstance(signals, list)
self.assertEqual(len(signals), 1)
for signal in signals:
self.assertIsInstance(signal, AnalogSignal)
self.assertEqual(signal.t_start, seq.t_start)
self.assertEqual(signal.sampling_period, seq.frame_duration)
self.assertEqual(signal.t_start, self.seq.t_start)
self.assertEqual(signal.sampling_period, self.seq.frame_duration)
with self.assertRaises(ValueError): # no pixels in region
zero_size_roi = RectangularRegionOfInterest(self.seq, 1, 1, 0, 0)
ImageSequence(
self.data, units="V", sampling_rate=500 * pq.Hz, spatial_scale=1 * pq.um
).signal_from_region(RectangularRegionOfInterest(1, 1, 0, 0))
).signal_from_region(zero_size_roi)
with self.assertRaises(ValueError):
ImageSequence(
self.data, units="V", sampling_rate=500 * pq.Hz, spatial_scale=1 * pq.um
Expand Down
20 changes: 13 additions & 7 deletions neo/test/coretest/test_regionofinterest.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
from neo.core.regionofinterest import RectangularRegionOfInterest, \
CircularRegionOfInterest,\
import quantities as pq
from neo.core.regionofinterest import (
RectangularRegionOfInterest,
CircularRegionOfInterest,
PolygonRegionOfInterest
)
from neo.core.imagesequence import ImageSequence
import unittest


class Test_CircularRegionOfInterest(unittest.TestCase):

def test_result(self):

self.assertEqual((CircularRegionOfInterest(6, 6, 1).pixels_in_region()),
seq = ImageSequence([[[]]], spatial_scale=1, frame_duration=20 * pq.ms)
self.assertEqual((CircularRegionOfInterest(seq, 6, 6, 1).pixels_in_region()),
[[6, 5], [5, 6], [6, 6]])
self.assertEqual((CircularRegionOfInterest(6, 6, 1.01).pixels_in_region()),
self.assertEqual((CircularRegionOfInterest(seq, 6, 6, 1.01).pixels_in_region()),
[[6, 5], [5, 6], [6, 6], [7, 6], [6, 7]])


class Test_RectangularRegionOfInterest(unittest.TestCase):

def test_result(self):
self.assertEqual(RectangularRegionOfInterest(5, 5, 2, 2).pixels_in_region(),
seq = ImageSequence([[[]]], spatial_scale=1, frame_duration=20 * pq.ms)
self.assertEqual(RectangularRegionOfInterest(seq, 5, 5, 2, 2).pixels_in_region(),
[[4, 4], [5, 4], [4, 5], [5, 5]])


class Test_PolygonRegionOfInterest(unittest.TestCase):

def test_result(self):
seq = ImageSequence([[[]]], spatial_scale=1, frame_duration=20 * pq.ms)
self.assertEqual(
PolygonRegionOfInterest((3, 3), (2, 5), (5, 5), (5, 1), (1, 1)).pixels_in_region(),
PolygonRegionOfInterest(seq, (3, 3), (2, 5), (5, 5), (5, 1), (1, 1)).pixels_in_region(),
[(1, 1), (2, 1), (3, 1), (4, 1), (2, 2), (3, 2),
(4, 2), (3, 3), (4, 3), (3, 4), (4, 4)]
)
Expand Down

0 comments on commit 54d65ef

Please sign in to comment.