From fcde71810dd089065562d305f6d370c3ca7a112d Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Thu, 27 Jul 2023 18:10:51 +0200 Subject: [PATCH] make the handling of RegionOfInterest subclasses consistent with ChannelView i.e. the ROI - contains a reference to the underlying data object - is not stored in a Block directly - can be stored in a Group - can have annotations --- neo/core/baseneo.py | 6 +++- neo/core/block.py | 8 ----- neo/core/group.py | 11 ++++++- neo/core/imagesequence.py | 6 ++-- neo/core/regionofinterest.py | 36 +++++++++++++++++++--- neo/test/coretest/test_group.py | 12 +++++++- neo/test/coretest/test_imagesequence.py | 23 +++++++------- neo/test/coretest/test_regionofinterest.py | 20 +++++++----- 8 files changed, 85 insertions(+), 37 deletions(-) diff --git a/neo/core/baseneo.py b/neo/core/baseneo.py index 43e6446da..093a43f42 100644 --- a/neo/core/baseneo.py +++ b/neo/core/baseneo.py @@ -156,7 +156,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: diff --git a/neo/core/block.py b/neo/core/block.py index baa6f64f4..93a0f4b1c 100644 --- a/neo/core/block.py +++ b/neo/core/block.py @@ -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 @@ -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"), @@ -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): ''' diff --git a/neo/core/group.py b/neo/core/group.py index 930e810b7..d12aca9e7 100644 --- a/neo/core/group.py +++ b/neo/core/group.py @@ -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): @@ -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 = ('Segment', 'Group') _parent_objects = ('Block',) @@ -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) @@ -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), diff --git a/neo/core/imagesequence.py b/neo/core/imagesequence.py index 231a4a204..cd96943d9 100644 --- a/neo/core/imagesequence.py +++ b/neo/core/imagesequence.py @@ -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): @@ -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): @@ -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 diff --git a/neo/core/regionofinterest.py b/neo/core/regionofinterest.py index cdf463653..458fb7067 100644 --- a/neo/core/regionofinterest.py +++ b/neo/core/regionofinterest.py @@ -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): @@ -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 @@ -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 @@ -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): diff --git a/neo/test/coretest/test_group.py b/neo/test/coretest/test_group.py index 48c3ab00a..800fecdaf 100644 --- a/neo/test/coretest/test_group.py +++ b/neo/test/coretest/test_group.py @@ -16,6 +16,8 @@ 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): @@ -23,6 +25,7 @@ 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", @@ -34,14 +37,19 @@ 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, self.test_segment] + objects = [self.test_view, self.test_signal, self.test_segment, self.test_image_seq, self.roi] objects.extend(self.test_spiketrains) group = Group(objects) @@ -49,6 +57,8 @@ def test_create_group(self): 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 assert group.segments[0].analogsignals[0] is self.test_signal diff --git a/neo/test/coretest/test_imagesequence.py b/neo/test/coretest/test_imagesequence.py index 4928fe515..a4fd8e768 100644 --- a/neo/test/coretest/test_imagesequence.py +++ b/neo/test/coretest/test_imagesequence.py @@ -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, @@ -71,8 +71,7 @@ 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([]) @@ -80,27 +79,29 @@ def fake_region_of_interest(self): 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 diff --git a/neo/test/coretest/test_regionofinterest.py b/neo/test/coretest/test_regionofinterest.py index b6ea4eba3..8f538c6e1 100644 --- a/neo/test/coretest/test_regionofinterest.py +++ b/neo/test/coretest/test_regionofinterest.py @@ -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)] )