Coverage for ibllib/io/extractors/widefield.py: 91%
119 statements
« prev ^ index » next coverage.py v7.7.0, created at 2025-03-17 15:25 +0000
« prev ^ index » next coverage.py v7.7.0, created at 2025-03-17 15:25 +0000
1"""Data extraction from widefield binary file"""
3import logging
4import numpy as np
5import shutil
6from pathlib import Path
7import pandas as pd
9import ibllib.exceptions as err
10import ibllib.io.extractors.base as extractors_base
11from ibllib.io.extractors.ephys_fpga import get_sync_fronts, get_sync_and_chn_map
12from ibllib.io.video import get_video_meta
14_logger = logging.getLogger('ibllib')
16try:
17 import wfield.cli as wfield_cli
18except ImportError:
19 _logger.warning('wfield not installed')
21try:
22 from labcams.io import parse_cam_log
23except ImportError:
24 _logger.warning('labcams not installed')
26_logger = logging.getLogger('ibllib')
28"""Available LEDs for Widefield Imaging"""
29LIGHT_SOURCE_MAP = {
30 0: 'None',
31 405: 'Violet',
32 470: 'Blue',
33}
35DEFAULT_WIRING_MAP = {
36 5: 470,
37 6: 405
38}
41class Widefield(extractors_base.BaseExtractor):
42 save_names = (None, None, None, 'widefieldChannels.frameAverage.npy', 'widefieldU.images.npy', 'widefieldSVT.uncorrected.npy',
43 None, None, 'widefieldSVT.haemoCorrected.npy', 'imaging.times.npy', 'imaging.imagingLightSource.npy',
44 'imagingLightSource.properties.htsv')
45 raw_names = ('motioncorrect_2_540_640_uint16.bin', 'motion_correction_shifts.npy', 'motion_correction_rotation.npy',
46 'frames_average.npy', 'U.npy', 'SVT.npy', 'rcoeffs.npy', 'T.npy', 'SVTcorr.npy', 'timestamps.npy', 'led.npy',
47 'led_properties.htsv')
48 var_names = ()
50 def __init__(self, *args, **kwargs):
51 """An extractor for all widefield data"""
52 super().__init__(*args, **kwargs) 1bhfcaed
53 self.data_path = self.session_path.joinpath('raw_widefield_data') 1bhfcaed
54 self.default_path = 'alf/widefield' 1bhfcaed
56 def _channel_meta(self, light_source_map=None):
57 """
58 Return table of light source wavelengths and corresponding colour labels.
60 Parameters
61 ----------
62 light_source_map : dict
63 An optional map of light source wavelengths (nm) used and their corresponding colour name.
65 Returns
66 -------
67 pandas.DataFrame
68 A sorted table of wavelength and colour name.
69 """
70 light_source_map = light_source_map or LIGHT_SOURCE_MAP 1hfa
71 names = ('wavelength', 'color') 1hfa
72 meta = pd.DataFrame(sorted(light_source_map.items()), columns=names) 1hfa
73 meta.index.rename('channel_id', inplace=True) 1hfa
74 return meta 1hfa
76 def _channel_wiring(self):
77 try: 1hfa
78 wiring = pd.read_csv(self.data_path.joinpath('widefieldChannels.wiring.htsv'), sep='\t') 1hfa
79 except FileNotFoundError: 1f
80 _logger.warning('LED wiring map not found, using default') 1f
81 wiring = pd.DataFrame(DEFAULT_WIRING_MAP.items(), columns=('LED', 'wavelength')) 1f
83 return wiring 1hfa
85 def _extract(self, extract_timestamps=True, save=False, **kwargs):
86 """
87 NB: kwargs should be loaded from meta file
88 Parameters
89 ----------
90 n_channels
91 dtype
92 shape
93 kwargs
95 Returns
96 -------
98 """
99 self.preprocess(**kwargs) 1c
100 if extract_timestamps: 1c
101 _ = self.sync_timestamps(save=save)
103 return None 1c
105 def _save(self, data=None, path_out=None):
107 if not path_out: 1gc
108 path_out = self.session_path.joinpath(self.default_path) 1gc
109 path_out.mkdir(exist_ok=True, parents=True) 1gc
111 new_files = [] 1gc
112 if not self.data_path.exists(): 1gc
113 _logger.warning(f'Path does not exist: {self.data_path}')
114 return new_files
116 for before, after in zip(self.raw_names, self.save_names): 1gc
117 if after is None: 1gc
118 continue 1gc
119 else:
120 try: 1gc
121 file_orig = next(self.data_path.glob(before)) 1gc
122 file_new = path_out.joinpath(after) 1gc
123 shutil.move(file_orig, file_new) 1gc
124 new_files.append(file_new) 1gc
125 except StopIteration: 1c
126 _logger.warning(f'File not found: {before}') 1c
128 return new_files 1gc
130 def preprocess(self, fs=30, functional_channel=0, nbaseline_frames=30, k=200, nchannels=2):
132 # MOTION CORRECTION
133 wfield_cli._motion(str(self.data_path), nchannels=nchannels, plot_ext='.png') 1c
134 # COMPUTE AVERAGE FOR BASELINE
135 wfield_cli._baseline(str(self.data_path), nbaseline_frames, nchannels=nchannels) 1c
136 # DATA REDUCTION
137 wfield_cli._decompose(str(self.data_path), k=k, nchannels=nchannels) 1c
138 # HAEMODYNAMIC CORRECTION
139 # check if it is 2 channel
140 dat = wfield_cli.load_stack(str(self.data_path), nchannels=nchannels) 1c
141 if dat.shape[1] == 2: 1c
142 del dat 1c
143 wfield_cli._hemocorrect(str(self.data_path), fs=fs, functional_channel=functional_channel, plot_ext='.png') 1c
145 def remove_files(self, file_prefix='motion'):
146 motion_files = self.data_path.glob(f'{file_prefix}*') 1ic
147 for file in motion_files: 1ic
148 _logger.info(f'Removing {file}') 1ic
149 file.unlink() 1ic
151 def sync_timestamps(self, bin_exists=False, save=False, save_paths=None, sync_collection='raw_sync_data', **kwargs):
153 if save and save_paths: 1jaed
154 assert len(save_paths) == 3, 'Must provide save_path as list with 3 paths' 1jaed
155 for save_path in save_paths: 1aed
156 Path(save_path).parent.mkdir(parents=True, exist_ok=True) 1aed
158 # Load in fpga sync
159 fpga_sync, chmap = get_sync_and_chn_map(self.session_path, sync_collection) 1aed
160 fpga_led = get_sync_fronts(fpga_sync, chmap['frame_trigger']) 1aed
161 fpga_led_up = fpga_led['times'][fpga_led['polarities'] == 1] # only consider up pulse times 1aed
163 # Load in camlog sync
164 logdata, led, sync, ncomm = parse_cam_log(next(self.data_path.glob('*.camlog')), readTeensy=True) 1aed
165 assert led.frame.is_monotonic_increasing 1aed
167 if led.frame.size != fpga_led_up.size: 1aed
168 _logger.warning(f'Sync mismatch by {np.abs(led.frame.size - fpga_led_up.size)} '
169 f'NIDQ sync times: {fpga_led_up.size}, LED frame times {led.frame.size}')
170 raise ValueError('Sync mismatch')
172 # Get video meta data to check number of widefield frames
173 video_path = next(self.data_path.glob('imaging.frames*.mov')) 1aed
174 video_meta = get_video_meta(video_path) 1aed
176 # Check for differences between video and ttl (in some cases we expect there to be extra ttl than frame, this is okay)
177 diff = len(led) - video_meta.length 1aed
178 if diff < 0: 1aed
179 raise ValueError('More video frames than led frames detected') 1e
180 if diff > 2: 1ad
181 raise ValueError('Led frames and video frames differ by more than 2') 1d
183 # take the timestamps as those recorded on fpga, no need to do any sycning
184 widefield_times = fpga_led_up[0:video_meta.length] 1a
186 # Now extract the LED channels and meta data
187 # Load channel meta and wiring map
188 channel_meta_map = self._channel_meta(kwargs.get('light_source_map')) 1a
189 channel_wiring = self._channel_wiring() 1a
190 channel_id = np.empty_like(led.led.values) 1a
192 for _, d in channel_wiring.iterrows(): 1a
193 mask = led.led.values == d['LED'] 1a
194 if np.sum(mask) == 0: 1a
195 raise err.WidefieldWiringException
196 channel_id[mask] = channel_meta_map.get(channel_meta_map['wavelength'] == d['wavelength']).index[0] 1a
198 if save: 1a
199 save_time = save_paths[0] if save_paths else self.data_path.joinpath('timestamps.npy') 1a
200 save_led = save_paths[1] if save_paths else self.data_path.joinpath('led.npy') 1a
201 save_meta = save_paths[2] if save_paths else self.data_path.joinpath('led_properties.htsv') 1a
202 save_paths = [save_time, save_led, save_meta] 1a
203 np.save(save_time, widefield_times) 1a
204 np.save(save_led, channel_id) 1a
205 channel_meta_map.to_csv(save_meta, sep='\t') 1a
207 return save_paths 1a
208 else:
209 return widefield_times, channel_id, channel_meta_map