Skip to content

Commit bb78256

Browse files
authored
Support extraction of multi-depth frame times using either neural_frames or volume_counter (#961)
Resolves issue #916 and issue #771
1 parent 20c4de3 commit bb78256

File tree

3 files changed

+58
-11
lines changed

3 files changed

+58
-11
lines changed

ibllib/io/extractors/default_channel_maps.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@
6262
'audio': 4,
6363
'bpod': 5,
6464
'rotary_encoder': 6,
65-
'neural_frames': 7}
65+
'neural_frames': 7,
66+
'volume_counter': 8}
6667
}
6768
}
6869

ibllib/io/extractors/mesoscope.py

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Mesoscope (timeline) data extraction."""
22
import logging
3+
from itertools import chain
34

45
import numpy as np
56
from scipy.signal import find_peaks
@@ -625,7 +626,7 @@ def __init__(self, session_path, n_FOVs):
625626
self.var_names = [f'{x}_{y.lower()}' for x in self.var_names for y in fov]
626627
self.save_names = [f'{y}/{x}' for x in self.save_names for y in fov]
627628

628-
def _extract(self, sync=None, chmap=None, device_collection='raw_imaging_data', events=None):
629+
def _extract(self, sync=None, chmap=None, device_collection='raw_imaging_data', events=None, use_volume_counter=False):
629630
"""
630631
Extract the frame timestamps for each individual field of view (FOV) and the time offsets
631632
for each line scan.
@@ -636,6 +637,10 @@ def _extract(self, sync=None, chmap=None, device_collection='raw_imaging_data',
636637
rawImagingData.times file). The field of view (FOV) shifts are then applied to these
637638
timestamps for each field of view and provided together with the line shifts.
638639
640+
Note that for single plane sessions, the 'neural_frames' and 'volume_counter' channels are
641+
identical. For multi-depth sessions, 'neural_frames' contains the frame times for each
642+
depth acquired.
643+
639644
Parameters
640645
----------
641646
sync : one.alf.io.AlfBunch
@@ -649,14 +654,22 @@ def _extract(self, sync=None, chmap=None, device_collection='raw_imaging_data',
649654
events : pandas.DataFrame
650655
A table of software events, with columns {'time_timeline' 'name_timeline',
651656
'event_timeline'}.
657+
use_volume_counter : bool
658+
If True, use the 'volume_counter' channel to extract the frame times. On the scale of
659+
calcium dynamics, it shouldn't matter whether we read specifically the timing of each
660+
slice, or assume that they are equally spaced between the volume_counter pulses. But
661+
in cases where each depth doesn't have the same nr of FOVs / scanlines, some depths
662+
will be faster than others, so it would be better to read out the neural frames for
663+
the purpose of computing the correct timeshifts per line. This can be set to True
664+
for legacy extractions.
652665
653666
Returns
654667
-------
655668
list of numpy.array
656669
A list of timestamps for each FOV and the time offsets for each line scan.
657670
"""
671+
volume_times = sync['times'][sync['channels'] == chmap['volume_counter']]
658672
frame_times = sync['times'][sync['channels'] == chmap['neural_frames']]
659-
660673
# imaging_start_time = datetime.datetime(*map(round, self.rawImagingData.meta['acquisitionStartTime']))
661674
if isinstance(device_collection, str):
662675
device_collection = [device_collection]
@@ -671,10 +684,11 @@ def _extract(self, sync=None, chmap=None, device_collection='raw_imaging_data',
671684
# Calculate line shifts
672685
_, fov_time_shifts, line_time_shifts = self.get_timeshifts(imaging_data['meta'])
673686
assert len(fov_time_shifts) == self.n_FOVs, f'unexpected number of FOVs for {collection}'
687+
vts = volume_times[np.logical_and(volume_times >= tmin, volume_times <= tmax)]
674688
ts = frame_times[np.logical_and(frame_times >= tmin, frame_times <= tmax)]
675-
assert ts.size >= imaging_data[
676-
'times_scanImage'].size, (f"fewer DAQ timestamps for {collection} than expected: "
677-
f"DAQ/frames = {ts.size}/{imaging_data['times_scanImage'].size}")
689+
assert ts.size >= imaging_data['times_scanImage'].size, \
690+
(f'fewer DAQ timestamps for {collection} than expected: '
691+
f'DAQ/frames = {ts.size}/{imaging_data["times_scanImage"].size}')
678692
if ts.size > imaging_data['times_scanImage'].size:
679693
_logger.warning(
680694
'More DAQ frame times detected for %s than were found in the raw image data.\n'
@@ -683,8 +697,32 @@ def _extract(self, sync=None, chmap=None, device_collection='raw_imaging_data',
683697
'when image data is corrupt, or when frames are not written to file.',
684698
collection, ts.size, imaging_data['times_scanImage'].size)
685699
_logger.info('Dropping last %i frame times for %s', ts.size - imaging_data['times_scanImage'].size, collection)
700+
vts = vts[vts < ts[imaging_data['times_scanImage'].size]]
686701
ts = ts[:imaging_data['times_scanImage'].size]
687-
fov_times.append([ts + offset for offset in fov_time_shifts])
702+
703+
# A 'slice_id' is a ScanImage 'ROI', comprising a collection of 'scanfields' a.k.a. slices at different depths
704+
# The total number of 'scanfields' == len(imaging_data['meta']['FOV'])
705+
slice_ids = np.array([x['slice_id'] for x in imaging_data['meta']['FOV']])
706+
unique_areas, slice_counts = np.unique(slice_ids, return_counts=True)
707+
n_unique_areas = len(unique_areas)
708+
709+
if use_volume_counter:
710+
# This is the simple, less accurate way of extrating imaging times
711+
fov_times.append([vts + offset for offset in fov_time_shifts])
712+
else:
713+
if len(np.unique(slice_counts)) != 1:
714+
# A different number of depths per FOV may no longer be an issue with this new method
715+
# of extracting imaging times, but the below assertion is kept as it's not tested and
716+
# not implemented for a different number of scanlines per FOV
717+
_logger.warning(
718+
'different number of slices per area (i.e. scanfields per ROI) (%s).',
719+
' vs '.join(map(str, slice_counts)))
720+
# This gets the imaging times for each FOV, respecting the order of the scanfields in multidepth imaging
721+
fov_times.append(list(chain.from_iterable(
722+
[ts[i::n_unique_areas][:vts.size] + offset for offset in fov_time_shifts[:n_depths]]
723+
for i, n_depths in enumerate(slice_counts)
724+
)))
725+
688726
if not line_shifts:
689727
line_shifts = line_time_shifts
690728
else: # The line shifts should be the same across all imaging bouts
@@ -693,7 +731,7 @@ def _extract(self, sync=None, chmap=None, device_collection='raw_imaging_data',
693731
# Concatenate imaging timestamps across all bouts for each field of view
694732
fov_times = list(map(np.concatenate, zip(*fov_times)))
695733
n_fov_times, = set(map(len, fov_times))
696-
if n_fov_times != frame_times.size:
734+
if n_fov_times != volume_times.size:
697735
# This may happen if an experimenter deletes a raw_imaging_data folder
698736
_logger.debug('FOV timestamps length does not match neural frame count; imaging bout(s) likely missing')
699737
return fov_times + line_shifts
@@ -782,7 +820,7 @@ def get_timeshifts(raw_imaging_meta):
782820
Calculate the time shifts for each field of view (FOV) and the relative offsets for each
783821
scan line.
784822
785-
For a 2 scan field, 2 depth recording (so 4 FOVs):
823+
For a 2 area (i.e. 'ROI'), 2 depth recording (so 4 FOVs):
786824
787825
Frame 1, lines 1-512 correspond to FOV_00
788826
Frame 1, lines 551-1062 correspond to FOV_01
@@ -791,6 +829,13 @@ def get_timeshifts(raw_imaging_meta):
791829
Frame 3, lines 1-512 correspond to FOV_00
792830
...
793831
832+
All areas are acquired for each depth such that...
833+
834+
FOV_00 = area 1, depth 1
835+
FOV_01 = area 2, depth 1
836+
FOV_02 = area 1, depth 2
837+
FOV_03 = area 2, depth 2
838+
794839
Parameters
795840
----------
796841
raw_imaging_meta : dict

ibllib/pipes/mesoscope_tasks.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -875,7 +875,7 @@ def signature(self):
875875
}
876876
return signature
877877

878-
def _run(self):
878+
def _run(self, **kwargs):
879879
"""
880880
Extract the imaging times for all FOVs.
881881
@@ -901,9 +901,10 @@ def _run(self):
901901
self.rawImagingData['meta'] = mesoscope.patch_imaging_meta(self.rawImagingData['meta'])
902902
n_FOVs = len(self.rawImagingData['meta']['FOV'])
903903
sync, chmap = self.load_sync() # Extract sync data from raw DAQ data
904+
legacy = kwargs.get('legacy', False) # this option may be removed in the future once fully tested
904905
mesosync = mesoscope.MesoscopeSyncTimeline(self.session_path, n_FOVs)
905906
_, out_files = mesosync.extract(
906-
save=True, sync=sync, chmap=chmap, device_collection=collections, events=events)
907+
save=True, sync=sync, chmap=chmap, device_collection=collections, events=events, use_volume_counter=legacy)
907908
return out_files
908909

909910

0 commit comments

Comments
 (0)