Permalink
Please sign in to comment.
| @@ -0,0 +1,232 @@ | ||
| +"""Conversion tool from OpenBCI to MNE Raw Class""" | ||
| + | ||
| +# Authors: Teon Brooks <teon.brooks@gmail.com> | ||
| +# | ||
| +# License: BSD (3-clause) | ||
| + | ||
| +import warnings | ||
| +np = None | ||
| +try: | ||
| + import numpy as np | ||
| +except ImportError: | ||
| + raise ImportError('Numpy is needed to use function.') | ||
| +mne = None | ||
| +try: | ||
| + from mne.utils import verbose, logger | ||
| + from mne.io.meas_info import create_info | ||
| + from mne.io.base import _BaseRaw | ||
| +except ImportError: | ||
| + raise ImportError('MNE is needed to use function.') | ||
| + | ||
| +class RawOpenBCI(_BaseRaw): | ||
| + """Raw object from OpenBCI file | ||
| + | ||
| + Parameters | ||
| + ---------- | ||
| + input_fname : str | ||
| + Path to the OpenBCI file. | ||
| + montage : str | None | instance of Montage | ||
| + Path or instance of montage containing electrode positions. | ||
| + If None, sensor locations are (0,0,0). See the documentation of | ||
| + :func:`mne.channels.read_montage` for more information. | ||
| + eog : list or tuple | ||
| + Names of channels or list of indices that should be designated | ||
| + EOG channels. Default is None. | ||
| + misc : list or tuple | ||
| + List of indices that should be designated MISC channels. | ||
| + Default is (-3, -2, -1), which are the accelerator sensors. | ||
| + stim_channel : int | None | ||
| + The channel index (starting at 0). | ||
| + If None (default), there will be no stim channel added. | ||
| + scale : float | ||
| + The scaling factor for EEG data. Units for MNE are in volts. | ||
| + OpenBCI data are typically stored in microvolts. Default scale | ||
| + factor is 1e-6. | ||
| + sfreq : int | ||
| + The sampling frequency of the data. OpenBCI defaults are 250 Hz. | ||
| + missing_tol : int | ||
| + The tolerance for interpolating missing samples. Default is 1. If the | ||
| + number of contiguous missing samples is greater than tolerance, then | ||
| + values are marked as NaN. | ||
| + preload : bool | ||
| + If True, all data are loaded at initialization. | ||
| + If False, data are not read until save. | ||
| + verbose : bool, str, int, or None | ||
| + If not None, override default verbose level (see mne.verbose). | ||
| + | ||
| + | ||
| + See Also | ||
| + -------- | ||
| + mne.io.Raw : Documentation of attribute and methods. | ||
| + """ | ||
| + @verbose | ||
| + def __init__(self, input_fname, montage=None, eog=None, | ||
| + misc=(-3, -2, -1), stim_channel=None, scale=1e-6, sfreq=250, | ||
| + missing_tol=1, preload=True, verbose=None): | ||
| + | ||
| + bci_info = {'missing_tol': missing_tol, 'stim_channel': stim_channel} | ||
| + if not eog: | ||
| + eog = list() | ||
| + if not misc: | ||
| + misc = list() | ||
| + nsamps, nchan = self._get_data_dims(input_fname) | ||
| + | ||
| + last_samps = [nsamps - 1] | ||
| + ch_names = ['EEG %03d' % num for num in range(1, nchan + 1)] | ||
| + ch_types = ['eeg'] * nchan | ||
| + if misc: | ||
| + misc_names = ['MISC %03d' % ii for ii in range(1, len(misc) + 1)] | ||
| + misc_types = ['misc'] * len(misc) | ||
| + for ii, mi in enumerate(misc): | ||
| + ch_names[mi] = misc_names[ii] | ||
| + ch_types[mi] = misc_types[ii] | ||
| + if eog: | ||
| + eog_names = ['EOG %03d' % ii for ii in range(len(eog))] | ||
| + eog_types = ['eog'] * len(eog) | ||
| + for ii, ei in enumerate(eog): | ||
| + ch_names[ei] = eog_names[ii] | ||
| + ch_types[ei] = eog_types[ii] | ||
| + if stim_channel: | ||
| + ch_names[stim_channel] = 'STI 014' | ||
| + ch_types[stim_channel] = 'stim' | ||
| + | ||
| + # fix it for eog and misc marking | ||
| + info = create_info(ch_names, sfreq, ch_types, montage) | ||
| + super(RawOpenBCI, self).__init__(info, last_samps=last_samps, | ||
| + raw_extras=[bci_info], | ||
| + filenames=[input_fname], | ||
| + preload=False, verbose=verbose) | ||
| + # load data | ||
| + if preload: | ||
| + self.preload = preload | ||
| + logger.info('Reading raw data from %s...' % input_fname) | ||
| + self._data, _ = self._read_segment() | ||
| + | ||
| + def _read_segment_file(self, data, idx, offset, fi, start, stop, | ||
| + cals, mult): | ||
| + """Read a chunk of raw data""" | ||
| + input_fname = self._filenames[fi] | ||
| + data_ = np.genfromtxt(input_fname, delimiter=',', comments='%', | ||
| + skip_footer=1) | ||
| + """ | ||
| + Dealing with the missing data | ||
| + ----------------------------- | ||
| + When recording with OpenBCI over Bluetooth, it is possible for some of | ||
| + the data packets, samples, to not be recorded. This does not happen | ||
| + often but it poses a problem for maintaining proper sampling periods. | ||
| + OpenBCI data format combats this by providing a counter on the sample | ||
| + to know which ones are missing. | ||
| + | ||
| + Solution | ||
| + -------- | ||
| + Interpolate the missing samples by resampling the surrounding samples. | ||
| + 1. Find where the missing samples are. | ||
| + 2. Deal with the counter reset (resets after cycling a byte). | ||
| + 3. Resample given the diffs. | ||
| + 4. Insert resampled data in the array using the diff indices | ||
| + (index + 1). | ||
| + 5. If number of missing samples is greater than the missing_tol, Values | ||
| + are replaced with np.nan. | ||
| + """ | ||
| + # counter goes from 0 to 255, maxdiff is 255. | ||
| + # make diff one like others. | ||
| + missing_tol = self._raw_extras[fi]['missing_tol'] | ||
| + diff = np.abs(np.diff(data_[:, 0])) | ||
| + diff = np.mod(diff, 254) - 1 | ||
| + missing_idx = np.where(diff != 0)[0] | ||
| + missing_samps = diff[missing_idx].astype(int) | ||
| + | ||
| + if missing_samps.size: | ||
| + missing_nsamps = np.sum(missing_samps, dtype=int) | ||
| + missing_cumsum = np.insert(np.cumsum(missing_samps), 0, 0)[:-1] | ||
| + missing_data = np.empty((missing_nsamps, data_.shape[-1]), | ||
| + dtype=float) | ||
| + insert_idx = list() | ||
| + for idx_, nn, ii in zip(missing_idx, missing_samps, | ||
| + missing_cumsum): | ||
| + missing_data[ii:ii + nn] = np.mean(data_[(idx_, idx_ + 1), :]) | ||
| + if nn > missing_tol: | ||
| + missing_data[ii:ii + nn] *= np.nan | ||
| + warnings.warn('The number of missing samples exceeded the ' | ||
| + 'missing_tol threshold.') | ||
| + insert_idx.append([idx_] * nn) | ||
| + insert_idx = np.hstack(insert_idx) | ||
| + data_ = np.insert(data_, insert_idx, missing_data, axis=0) | ||
| + # data_ dimensions are samples by channels. transpose for MNE. | ||
| + data_ = data_[start:stop, 1:].T | ||
| + data[:, offset:offset + stop - start] = \ | ||
| + np.dot(mult, data_[idx]) if mult is not None else data_[idx] | ||
| + | ||
| + def _get_data_dims(self, input_fname): | ||
| + """Briefly scan the data file for info""" | ||
| + # raw data formatting is nsamps by nchans + counter | ||
| + data = np.genfromtxt(input_fname, delimiter=',', comments='%', | ||
| + skip_footer=1) | ||
| + diff = np.abs(np.diff(data[:, 0])) | ||
| + diff = np.mod(diff, 254) - 1 | ||
| + missing_idx = np.where(diff != 0)[0] | ||
| + missing_samps = diff[missing_idx].astype(int) | ||
| + nsamps, nchan = data.shape | ||
| + # add the missing samples | ||
| + nsamps += sum(missing_samps) | ||
| + # remove the tracker column | ||
| + nchan -= 1 | ||
| + del data | ||
| + | ||
| + return nsamps, nchan | ||
| + | ||
| + | ||
| +def read_raw_openbci(input_fname, montage=None, eog=None, misc=(-3, -2, -1), | ||
| + stim_channel=None, scale=1e-6, sfreq=250, missing_tol=1, | ||
| + preload=True, verbose=None): | ||
| + """Raw object from OpenBCI file | ||
| + | ||
| + Parameters | ||
| + ---------- | ||
| + input_fname : str | ||
| + Path to the OpenBCI file. | ||
| + montage : str | None | instance of Montage | ||
| + Path or instance of montage containing electrode positions. | ||
| + If None, sensor locations are (0,0,0). See the documentation of | ||
| + :func:`mne.channels.read_montage` for more information. | ||
| + eog : list or tuple | ||
| + Names of channels or list of indices that should be designated | ||
| + EOG channels. Default is None. | ||
| + misc : list or tuple | ||
| + List of indices that should be designated MISC channels. | ||
| + Default is (-3, -2, -1), which are the accelerator sensors. | ||
| + stim_channel : str | int | None | ||
| + The channel name or channel index (starting at 0). | ||
| + -1 corresponds to the last channel (default). | ||
| + If None, there will be no stim channel added. | ||
| + scale : float | ||
| + The scaling factor for EEG data. Units for MNE are in volts. | ||
| + OpenBCI data are typically stored in microvolts. Default scale | ||
| + factor is 1e-6. | ||
| + sfreq : int | ||
| + The sampling frequency of the data. OpenBCI defaults are 250 Hz. | ||
| + missing_tol : int | ||
| + The tolerance for interpolating missing samples. Default is 1. If the | ||
| + number of contiguous missing samples is greater than tolerance, then | ||
| + values are marked as NaN. | ||
| + preload : bool | ||
| + If True, all data are loaded at initialization. | ||
| + If False, data are not read until save. | ||
| + verbose : bool, str, int, or None | ||
| + If not None, override default verbose level (see mne.verbose). | ||
| + | ||
| + Returns | ||
| + ------- | ||
| + raw : Instance of RawOpenBCI | ||
| + A Raw object containing OpenBCI data. | ||
| + | ||
| + | ||
| + See Also | ||
| + -------- | ||
| + mne.io.Raw : Documentation of attribute and methods. | ||
| + """ | ||
| + raw = RawOpenBCI(input_fname=input_fname, montage=montage, eog=eog, | ||
| + misc=misc, stim_channel=stim_channel, scale=scale, | ||
| + sfreq=sfreq, missing_tol=missing_tol, preload=preload, | ||
| + verbose=verbose) | ||
| + return raw |
0 comments on commit
99eec5b