diff --git a/externals/mne_openbci.py b/externals/mne_openbci.py new file mode 100644 index 0000000..2d12c33 --- /dev/null +++ b/externals/mne_openbci.py @@ -0,0 +1,232 @@ +"""Conversion tool from OpenBCI to MNE Raw Class""" + +# Authors: Teon Brooks +# +# 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