Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make the handling of RegionOfInterest subclasses consistent with ChannelView #1313

Merged
merged 2 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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