Skip to content

Commit 57f8d07

Browse files
authored
Merge pull request #855 from apdavison/issue799
Fix for #799
2 parents 79df3c8 + b2d49e9 commit 57f8d07

File tree

5 files changed

+206
-76
lines changed

5 files changed

+206
-76
lines changed

neo/core/basesignal.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,10 @@ def _get_required_attributes(self, signal, units):
139139
'''
140140
required_attributes = {}
141141
for attr in self._necessary_attrs:
142-
if 'signal' == attr[0]:
143-
required_attributes[str(attr[0])] = signal
142+
if attr[0] == "signal":
143+
required_attributes["signal"] = signal
144+
elif attr[0] == "t_start":
145+
required_attributes["t_start"] = getattr(self, "t_start", 0.0 * pq.ms)
144146
else:
145147
required_attributes[str(attr[0])] = getattr(self, attr[0], None)
146148
required_attributes['units'] = units
@@ -176,7 +178,9 @@ def _copy_data_complement(self, other):
176178
all_attr = {self._recommended_attrs, self._necessary_attrs}
177179
for sub_at in all_attr:
178180
for attr in sub_at:
179-
if attr[0] != 'signal':
181+
if attr[0] == "t_start":
182+
setattr(self, attr[0], deepcopy(getattr(other, attr[0], 0.0 * pq.ms)))
183+
elif attr[0] != 'signal':
180184
setattr(self, attr[0], deepcopy(getattr(other, attr[0], None)))
181185
setattr(self, 'annotations', deepcopy(getattr(other, 'annotations', None)))
182186

neo/core/imagesequence.py

Lines changed: 133 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ class ImageSequence(BaseSignal):
4141
>>> img_sequence_array = [[[column for column in range(20)]for row in range(20)]
4242
... for frame in range(10)]
4343
>>> image_sequence = ImageSequence(img_sequence_array, units='V',
44-
... sampling_rate=1*pq.Hz, spatial_scale=1*pq.micrometer)
44+
... sampling_rate=1 * pq.Hz,
45+
... spatial_scale=1 * pq.micrometer)
4546
>>> image_sequence
46-
ImageSequence 10 frame with 20 px of height and 20 px of width; units V; datatype int64
47+
ImageSequence 10 frames with width 20 px and height 20 px; units V; datatype int64
4748
sampling rate: 1.0
4849
spatial_scale: 1.0
4950
>>> image_sequence.spatial_scale
@@ -53,12 +54,13 @@ class ImageSequence(BaseSignal):
5354
:image_data: (3D NumPy array, or a list of 2D arrays)
5455
The data itself
5556
:units: (quantity units)
56-
:sampling_rate: *or* **sampling_period** (quantity scalar) Number of
57+
:sampling_rate: *or* **frame_duration** (quantity scalar) Number of
5758
samples per unit time or
58-
interval beween to samples.
59+
duration of a single image frame.
5960
If both are specified, they are
6061
checked for consistency.
6162
:spatial_scale: (quantity scalar) size for a pixel.
63+
:t_start: (quantity scalar) Time when sequence begins. Default 0.
6264
6365
*Recommended attributes/properties*:
6466
:name: (str) A label for the dataset.
@@ -74,20 +76,29 @@ class ImageSequence(BaseSignal):
7476
7577
*Properties available on this object*:
7678
:sampling_rate: (quantity scalar) Number of samples per unit time.
77-
(1/:attr:`sampling_period`)
78-
:sampling_period: (quantity scalar) Interval between two samples.
79-
(1/:attr:`quantity scalar`)
80-
:spatial_scale: size of a pixel
79+
(1/:attr:`frame_duration`)
80+
:frame_duration: (quantity scalar) Duration of each image frame.
81+
(1/:attr:`sampling_rate`)
82+
:spatial_scale: Size of a pixel
83+
:duration: (Quantity) Sequence duration, read-only.
84+
(size * :attr:`frame_duration`)
85+
:t_stop: (quantity scalar) Time when sequence ends, read-only.
86+
(:attr:`t_start` + :attr:`duration`)
8187
"""
82-
_single_parent_objects = ('Segment',)
83-
_single_parent_attrs = ('segment',)
84-
_quantity_attr = 'image_data'
85-
_necessary_attrs = (('image_data', pq.Quantity, 3),
86-
('sampling_rate', pq.Quantity, 0),
87-
('spatial_scale', pq.Quantity, 0))
88+
89+
_single_parent_objects = ("Segment",)
90+
_single_parent_attrs = ("segment",)
91+
_quantity_attr = "image_data"
92+
_necessary_attrs = (
93+
("image_data", pq.Quantity, 3),
94+
("sampling_rate", pq.Quantity, 0),
95+
("spatial_scale", pq.Quantity, 0),
96+
("t_start", pq.Quantity, 0),
97+
)
8898
_recommended_attrs = BaseNeo._recommended_attrs
8999

90-
def __new__(cls, image_data, units=None, dtype=None, copy=True, spatial_scale=None, sampling_period=None,
100+
def __new__(cls, image_data, units=None, dtype=None, copy=True, t_start=0 * pq.s,
101+
spatial_scale=None, frame_duration=None,
91102
sampling_rate=None, name=None, description=None, file_origin=None,
92103
**annotations):
93104
"""
@@ -99,52 +110,58 @@ def __new__(cls, image_data, units=None, dtype=None, copy=True, spatial_scale=No
99110
__array_finalize__ is called on the new object.
100111
"""
101112
if spatial_scale is None:
102-
raise ValueError('spatial_scale is required')
113+
raise ValueError("spatial_scale is required")
103114

104115
image_data = np.stack(image_data)
105116
if len(image_data.shape) != 3:
106-
raise ValueError('list doesn\'t have the good number of dimension')
117+
raise ValueError("list doesn't have the correct number of dimensions")
107118

108119
obj = pq.Quantity(image_data, units=units, dtype=dtype, copy=copy).view(cls)
109120
obj.segment = None
110121
# function from analogsignal.py in neo/core directory
111-
obj.sampling_rate = _get_sampling_rate(sampling_rate, sampling_period)
122+
obj.sampling_rate = _get_sampling_rate(sampling_rate, frame_duration)
112123
obj.spatial_scale = spatial_scale
124+
if t_start is None:
125+
raise ValueError("t_start cannot be None")
126+
obj._t_start = t_start
113127

114128
return obj
115129

116-
def __init__(self, image_data, units=None, dtype=None, copy=True, spatial_scale=None, sampling_period=None,
130+
def __init__(self, image_data, units=None, dtype=None, copy=True, t_start=0 * pq.s,
131+
spatial_scale=None, frame_duration=None,
117132
sampling_rate=None, name=None, description=None, file_origin=None,
118133
**annotations):
119-
'''
134+
"""
120135
Initializes a newly constructed :class:`ImageSequence` instance.
121-
'''
122-
DataObject.__init__(self, name=name, file_origin=file_origin, description=description,
123-
**annotations)
136+
"""
137+
DataObject.__init__(
138+
self, name=name, file_origin=file_origin, description=description, **annotations
139+
)
124140

125141
def __array_finalize__spec(self, obj):
126142

127-
self.sampling_rate = getattr(obj, 'sampling_rate', None)
128-
self.spatial_scale = getattr(obj, 'spatial_scale', None)
129-
self.units = getattr(obj, 'units', None)
143+
self.sampling_rate = getattr(obj, "sampling_rate", None)
144+
self.spatial_scale = getattr(obj, "spatial_scale", None)
145+
self.units = getattr(obj, "units", None)
146+
self._t_start = getattr(obj, "_t_start", 0 * pq.s)
130147

131148
return obj
132149

133150
def signal_from_region(self, *region):
134151
"""
135-
Method that takes 1 or multiple regionofinterest, use the method of each region
136-
of interest to get the list of pixel to average.
137-
return a list of :class:`AnalogSignal` for each regionofinterest
152+
Method that takes 1 or multiple regionofinterest, uses the method of each region
153+
of interest to get the list of pixels to average.
154+
Return a list of :class:`AnalogSignal` for each regionofinterest
138155
"""
139156

140157
if len(region) == 0:
141-
raise ValueError('no region of interest have been given')
158+
raise ValueError("no regions of interest have been given")
142159

143160
region_pixel = []
144161
for i, b in enumerate(region):
145162
r = region[i].pixels_in_region()
146163
if not r:
147-
raise ValueError('region '+str(i)+'is empty')
164+
raise ValueError("region " + str(i) + "is empty")
148165
else:
149166
region_pixel.append(r)
150167
analogsignal_list = []
@@ -158,23 +175,29 @@ def signal_from_region(self, *region):
158175
for b in range(1, len(picture_data)):
159176
average += picture_data[b]
160177
data.append((average * 1.0) / len(i))
161-
analogsignal_list.append(AnalogSignal(data, units=self.units,
162-
sampling_rate=self.sampling_rate))
178+
analogsignal_list.append(
179+
AnalogSignal(
180+
data, units=self.units, t_start=self.t_start, sampling_rate=self.sampling_rate
181+
)
182+
)
163183

164184
return analogsignal_list
165185

166186
def _repr_pretty_(self, pp, cycle):
167-
'''
187+
"""
168188
Handle pretty-printing the :class:`ImageSequence`.
169-
'''
170-
pp.text("{cls} {frame} frame with {width} px of width and {height} px of height; "
171-
"units {units}; datatype {dtype} ".format(
172-
cls=self.__class__.__name__,
173-
frame=self.shape[0],
174-
height=self.shape[1],
175-
width=self.shape[2],
176-
units=self.units.dimensionality.string,
177-
dtype=self.dtype))
189+
"""
190+
pp.text(
191+
"{cls} {nframe} frames with width {width} px and height {height} px; "
192+
"units {units}; datatype {dtype} ".format(
193+
cls=self.__class__.__name__,
194+
nframe=self.shape[0],
195+
height=self.shape[1],
196+
width=self.shape[2],
197+
units=self.units.dimensionality.string,
198+
dtype=self.dtype,
199+
)
200+
)
178201

179202
def _pp(line):
180203
pp.breakable()
@@ -188,11 +211,75 @@ def _pp(line):
188211
_pp(line)
189212

190213
def _check_consistency(self, other):
191-
'''
214+
"""
192215
Check if the attributes of another :class:`ImageSequence`
193216
are compatible with this one.
194-
'''
217+
"""
195218
if isinstance(other, ImageSequence):
196-
for attr in ("sampling_rate", "spatial_scale"):
219+
for attr in ("sampling_rate", "spatial_scale", "t_start"):
197220
if getattr(self, attr) != getattr(other, attr):
198221
raise ValueError("Inconsistent values of %s" % attr)
222+
223+
# t_start attribute is handled as a property so type checking can be done
224+
@property
225+
def t_start(self):
226+
"""
227+
Time when sequence begins.
228+
"""
229+
return self._t_start
230+
231+
@t_start.setter
232+
def t_start(self, start):
233+
"""
234+
Setter for :attr:`t_start`
235+
"""
236+
if start is None:
237+
raise ValueError("t_start cannot be None")
238+
self._t_start = start
239+
240+
@property
241+
def duration(self):
242+
"""
243+
Sequence duration
244+
245+
(:attr:`size` * :attr:`frame_duration`)
246+
"""
247+
return self.shape[0] / self.sampling_rate
248+
249+
@property
250+
def t_stop(self):
251+
"""
252+
Time when Sequence ends.
253+
254+
(:attr:`t_start` + :attr:`duration`)
255+
"""
256+
return self.t_start + self.duration
257+
258+
@property
259+
def times(self):
260+
"""
261+
The time points of each frame in the sequence
262+
263+
(:attr:`t_start` + arange(:attr:`shape`)/:attr:`sampling_rate`)
264+
"""
265+
return self.t_start + np.arange(self.shape[0]) / self.sampling_rate
266+
267+
@property
268+
def frame_duration(self):
269+
"""
270+
Duration of a single image frame in the sequence.
271+
272+
(1/:attr:`sampling_rate`)
273+
"""
274+
return 1.0 / self.sampling_rate
275+
276+
@frame_duration.setter
277+
def frame_duration(self, duration):
278+
"""
279+
Setter for :attr:`frame_duration`
280+
"""
281+
if duration is None:
282+
raise ValueError("frame_duration cannot be None")
283+
elif not hasattr(duration, "units"):
284+
raise ValueError("frame_duration must have units")
285+
self.sampling_rate = 1.0 / duration

neo/io/nixio.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,10 +507,15 @@ def _nix_to_neo_imagesequence(self, nix_da_group):
507507
del neo_attrs["sampling_rate"]
508508
spatial_scale = neo_attrs["spatial_scale"]
509509
del neo_attrs["spatial_scale"]
510+
if "t_start" in neo_attrs:
511+
t_start = neo_attrs["t_start"]
512+
del neo_attrs["t_start"]
513+
else:
514+
t_start = 0.0 * pq.ms
510515

511516
neo_seq = ImageSequence(image_data=imgseq, sampling_rate=sampling_rate,
512517
spatial_scale=spatial_scale, units=unit,
513-
**neo_attrs)
518+
t_start=t_start, **neo_attrs)
514519

515520
self._neo_map[neo_attrs["nix_name"]] = neo_seq
516521
# all DAs reference the same sources
@@ -911,6 +916,9 @@ def _write_imagesequence(self, imgseq, nixblock, nixgroup):
911916
metadata["spatial_scale"] = imgseq.spatial_scale.magnitude.item()
912917
units = imgseq.spatial_scale.units
913918
metadata.props["spatial_scale"].unit = units_to_string(units)
919+
metadata["t_start"] = imgseq.t_start.magnitude.item()
920+
units = imgseq.t_start.units
921+
metadata.props["t_start"].unit = units_to_string(units)
914922

915923
nixdas.append(da)
916924
if nixgroup:

0 commit comments

Comments
 (0)