Skip to content

Commit 54d65ef

Browse files
authored
Merge pull request #1313 from apdavison/fix-regionsofinterest
make the handling of RegionOfInterest subclasses consistent with ChannelView
2 parents 32c3b99 + b4f0029 commit 54d65ef

8 files changed

+85
-37
lines changed

neo/core/baseneo.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,11 @@ def _container_name(class_name):
154154
referenced by `block.segments`. The attribute name `segments` is
155155
obtained by calling `_container_name_plural("Segment")`.
156156
"""
157-
return _reference_name(class_name) + 's'
157+
if "RegionOfInterest" in class_name:
158+
# this is a hack, pending a more principled way to handle this
159+
return "regionsofinterest"
160+
else:
161+
return _reference_name(class_name) + 's'
158162

159163

160164
class BaseNeo:

neo/core/block.py

-8
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from neo.core.container import Container, unique_objs
1313
from neo.core.group import Group
1414
from neo.core.objectlist import ObjectList
15-
from neo.core.regionofinterest import RegionOfInterest
1615
from neo.core.segment import Segment
1716

1817

@@ -91,7 +90,6 @@ def __init__(self, name=None, description=None, file_origin=None,
9190
self.index = index
9291
self._segments = ObjectList(Segment, parent=self)
9392
self._groups = ObjectList(Group, parent=self)
94-
self._regionsofinterest = ObjectList(RegionOfInterest, parent=self)
9593

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

108-
regionsofinterest = property(
109-
fget=lambda self: self._get_object_list("_regionsofinterest"),
110-
fset=lambda self, value: self._set_object_list("_regionsofinterest", value),
111-
doc="list of RegionOfInterest objects contained in this block"
112-
)
113-
114106
@property
115107
def data_children_recur(self):
116108
'''

neo/core/group.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from neo.core.segment import Segment
1919
from neo.core.spiketrainlist import SpikeTrainList
2020
from neo.core.view import ChannelView
21+
from neo.core.regionofinterest import RegionOfInterest
2122

2223

2324
class Group(Container):
@@ -49,7 +50,8 @@ class Group(Container):
4950
"""
5051
_data_child_objects = (
5152
'AnalogSignal', 'IrregularlySampledSignal', 'SpikeTrain',
52-
'Event', 'Epoch', 'ChannelView', 'ImageSequence'
53+
'Event', 'Epoch', 'ChannelView', 'ImageSequence', 'CircularRegionOfInterest',
54+
'RectangularRegionOfInterest', 'PolygonRegionOfInterest'
5355
)
5456
_container_child_objects = ('Group',)
5557
_parent_objects = ('Block',)
@@ -69,6 +71,7 @@ def __init__(self, objects=None, name=None, description=None, file_origin=None,
6971
self._epochs = ObjectList(Epoch)
7072
self._channelviews = ObjectList(ChannelView)
7173
self._imagesequences = ObjectList(ImageSequence)
74+
self._regionsofinterest = ObjectList(RegionOfInterest)
7275
self._segments = ObjectList(Segment) # to remove?
7376
self._groups = ObjectList(Group)
7477

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

125+
regionsofinterest = property(
126+
fget=lambda self: self._get_object_list("_regionsofinterest"),
127+
fset=lambda self, value: self._set_object_list("_regionsofinterest", value),
128+
doc="list of RegionOfInterest objects contained in this group"
129+
)
130+
122131
spiketrains = property(
123132
fget=lambda self: self._get_object_list("_spiketrains"),
124133
fset=lambda self, value: self._set_object_list("_spiketrains", value),

neo/core/imagesequence.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class ImageSequence(BaseSignal):
9797
)
9898
_recommended_attrs = BaseNeo._recommended_attrs
9999

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

128128
return obj
129129

130-
def __init__(self, image_data, units=None, dtype=None, copy=True, t_start=0 * pq.s,
130+
def __init__(self, image_data, units=pq.dimensionless, dtype=None, copy=True, t_start=0 * pq.s,
131131
spatial_scale=None, frame_duration=None,
132132
sampling_rate=None, name=None, description=None, file_origin=None,
133133
**annotations):
@@ -142,7 +142,7 @@ def __array_finalize__spec(self, obj):
142142

143143
self.sampling_rate = getattr(obj, "sampling_rate", None)
144144
self.spatial_scale = getattr(obj, "spatial_scale", None)
145-
self.units = getattr(obj, "units", None)
145+
self.units = getattr(obj, "units", pq.dimensionless)
146146
self._t_start = getattr(obj, "_t_start", 0 * pq.s)
147147

148148
return obj

neo/core/regionofinterest.py

+31-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,32 @@
11
from math import floor, ceil
22

33
from neo.core.baseneo import BaseNeo
4+
from neo.core.imagesequence import ImageSequence
45

56

67
class RegionOfInterest(BaseNeo):
78
"""Abstract base class"""
8-
pass
9+
10+
_parent_objects = ('Group',)
11+
_parent_attrs = ('group',)
12+
_necessary_attrs = (
13+
('obj', ('ImageSequence', ), 1),
14+
)
15+
16+
def __init__(self, image_sequence, name=None, description=None, file_origin=None, **annotations):
17+
super().__init__(name=name, description=description,
18+
file_origin=file_origin, **annotations)
19+
20+
if not (isinstance(image_sequence, ImageSequence) or (
21+
hasattr(image_sequence, "proxy_for") and issubclass(image_sequence.proxy_for, ImageSequence))):
22+
raise ValueError("Can only take a RegionOfInterest of an ImageSequence")
23+
self.image_sequence = image_sequence
24+
25+
def resolve(self):
26+
"""
27+
Return a signal from within this region of the underlying ImageSequence.
28+
"""
29+
return self.image_sequence.signal_from_region(self)
930

1031

1132
class CircularRegionOfInterest(RegionOfInterest):
@@ -23,8 +44,9 @@ class CircularRegionOfInterest(RegionOfInterest):
2344
Radius of the ROI in pixels
2445
"""
2546

26-
def __init__(self, x, y, radius):
27-
47+
def __init__(self, image_sequence, x, y, radius, name=None, description=None,
48+
file_origin=None, **annotations):
49+
super().__init__(image_sequence, name, description, file_origin, **annotations)
2850
self.y = y
2951
self.x = x
3052
self.radius = radius
@@ -72,7 +94,9 @@ class RectangularRegionOfInterest(RegionOfInterest):
7294
Height (y-direction) of the ROI in pixels
7395
"""
7496

75-
def __init__(self, x, y, width, height):
97+
def __init__(self, image_sequence, x, y, width, height, name=None, description=None,
98+
file_origin=None, **annotations):
99+
super().__init__(image_sequence, name, description, file_origin, **annotations)
76100
self.x = x
77101
self.y = y
78102
self.width = width
@@ -115,7 +139,9 @@ class PolygonRegionOfInterest(RegionOfInterest):
115139
of the vertices of the polygon
116140
"""
117141

118-
def __init__(self, *vertices):
142+
def __init__(self, image_sequence, *vertices, name=None, description=None,
143+
file_origin=None, **annotations):
144+
super().__init__(image_sequence, name, description, file_origin, **annotations)
119145
self.vertices = vertices
120146

121147
def polygon_ray_casting(self, bounding_points, bounding_box_positions):

neo/test/coretest/test_group.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@
1616
from neo.core.view import ChannelView
1717
from neo.core.group import Group
1818
from neo.core.block import Block
19+
from neo.core.imagesequence import ImageSequence
20+
from neo.core.regionofinterest import CircularRegionOfInterest
1921

2022

2123
class TestGroup(unittest.TestCase):
2224

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

4351
def test_create_group(self):
44-
objects = [self.test_view, self.test_signal]
52+
objects = [self.test_view, self.test_signal, self.test_image_seq, self.roi]
4553
objects.extend(self.test_spiketrains)
4654
group = Group(objects)
4755

4856
assert group.analogsignals[0] is self.test_signal
4957
assert group.spiketrains[0] is self.test_spiketrains[0]
5058
assert group.spiketrains[1] is self.test_spiketrains[1]
5159
assert group.channelviews[0] is self.test_view
60+
assert group.imagesequences[0] is self.test_image_seq
61+
assert group.regionsofinterest[0] is self.roi
5262
assert len(group.irregularlysampledsignals) == 0
5363

5464
def test_create_empty_group(self):

neo/test/coretest/test_imagesequence.py

+12-11
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def test_error_spatial_scale(self):
3939

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

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

7272

7373
class TestMethodImageSequence(unittest.TestCase):
74-
def fake_region_of_interest(self):
75-
self.rect_ROI = RectangularRegionOfInterest(2, 2, 2, 2)
74+
def _create_test_objects(self):
7675
self.data = []
7776
for frame in range(25):
7877
self.data.append([])
7978
for y in range(5):
8079
self.data[frame].append([])
8180
for x in range(5):
8281
self.data[frame][y].append(x)
83-
84-
def test_signal_from_region(self):
85-
self.fake_region_of_interest()
86-
seq = ImageSequence(
82+
self.seq = ImageSequence(
8783
self.data,
8884
units="V",
8985
sampling_rate=500 * pq.Hz,
9086
t_start=250 * pq.ms,
9187
spatial_scale=1 * pq.um,
9288
)
93-
signals = seq.signal_from_region(self.rect_ROI)
89+
self.rect_ROI = RectangularRegionOfInterest(self.seq, 2, 2, 2, 2)
90+
91+
def test_signal_from_region(self):
92+
self._create_test_objects()
93+
signals = self.seq.signal_from_region(self.rect_ROI)
9494
self.assertIsInstance(signals, list)
9595
self.assertEqual(len(signals), 1)
9696
for signal in signals:
9797
self.assertIsInstance(signal, AnalogSignal)
98-
self.assertEqual(signal.t_start, seq.t_start)
99-
self.assertEqual(signal.sampling_period, seq.frame_duration)
98+
self.assertEqual(signal.t_start, self.seq.t_start)
99+
self.assertEqual(signal.sampling_period, self.seq.frame_duration)
100100
with self.assertRaises(ValueError): # no pixels in region
101+
zero_size_roi = RectangularRegionOfInterest(self.seq, 1, 1, 0, 0)
101102
ImageSequence(
102103
self.data, units="V", sampling_rate=500 * pq.Hz, spatial_scale=1 * pq.um
103-
).signal_from_region(RectangularRegionOfInterest(1, 1, 0, 0))
104+
).signal_from_region(zero_size_roi)
104105
with self.assertRaises(ValueError):
105106
ImageSequence(
106107
self.data, units="V", sampling_rate=500 * pq.Hz, spatial_scale=1 * pq.um

neo/test/coretest/test_regionofinterest.py

+13-7
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,37 @@
1-
from neo.core.regionofinterest import RectangularRegionOfInterest, \
2-
CircularRegionOfInterest,\
1+
import quantities as pq
2+
from neo.core.regionofinterest import (
3+
RectangularRegionOfInterest,
4+
CircularRegionOfInterest,
35
PolygonRegionOfInterest
6+
)
7+
from neo.core.imagesequence import ImageSequence
48
import unittest
59

610

711
class Test_CircularRegionOfInterest(unittest.TestCase):
812

913
def test_result(self):
10-
11-
self.assertEqual((CircularRegionOfInterest(6, 6, 1).pixels_in_region()),
14+
seq = ImageSequence([[[]]], spatial_scale=1, frame_duration=20 * pq.ms)
15+
self.assertEqual((CircularRegionOfInterest(seq, 6, 6, 1).pixels_in_region()),
1216
[[6, 5], [5, 6], [6, 6]])
13-
self.assertEqual((CircularRegionOfInterest(6, 6, 1.01).pixels_in_region()),
17+
self.assertEqual((CircularRegionOfInterest(seq, 6, 6, 1.01).pixels_in_region()),
1418
[[6, 5], [5, 6], [6, 6], [7, 6], [6, 7]])
1519

1620

1721
class Test_RectangularRegionOfInterest(unittest.TestCase):
1822

1923
def test_result(self):
20-
self.assertEqual(RectangularRegionOfInterest(5, 5, 2, 2).pixels_in_region(),
24+
seq = ImageSequence([[[]]], spatial_scale=1, frame_duration=20 * pq.ms)
25+
self.assertEqual(RectangularRegionOfInterest(seq, 5, 5, 2, 2).pixels_in_region(),
2126
[[4, 4], [5, 4], [4, 5], [5, 5]])
2227

2328

2429
class Test_PolygonRegionOfInterest(unittest.TestCase):
2530

2631
def test_result(self):
32+
seq = ImageSequence([[[]]], spatial_scale=1, frame_duration=20 * pq.ms)
2733
self.assertEqual(
28-
PolygonRegionOfInterest((3, 3), (2, 5), (5, 5), (5, 1), (1, 1)).pixels_in_region(),
34+
PolygonRegionOfInterest(seq, (3, 3), (2, 5), (5, 5), (5, 1), (1, 1)).pixels_in_region(),
2935
[(1, 1), (2, 1), (3, 1), (4, 1), (2, 2), (3, 2),
3036
(4, 2), (3, 3), (4, 3), (3, 4), (4, 4)]
3137
)

0 commit comments

Comments
 (0)