├── eegp ├── core │ ├── __init__.py │ ├── metadata.py │ ├── load.py │ └── preprocess.py ├── __init__.py ├── path │ ├── __init__.py │ ├── path.py │ └── utils.py └── paradigms │ ├── __init__.py │ ├── utils.py │ ├── delaymatch.py │ ├── physionet.py │ └── base.py ├── .gitignore ├── LICENSE ├── example ├── remove_eog_mi.py ├── delaymatch.py └── delay_srt.py └── README.md /eegp/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /eegp/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = 0.3 2 | -------------------------------------------------------------------------------- /eegp/path/__init__.py: -------------------------------------------------------------------------------- 1 | from .path import FilePath 2 | from .utils import check_paths, create_dir 3 | -------------------------------------------------------------------------------- /eegp/paradigms/__init__.py: -------------------------------------------------------------------------------- 1 | from .delaymatch import LetterDelayMatch 2 | from .physionet import PhysioNetMI 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows: 2 | Thumbs.db 3 | ehthumbs.db 4 | Desktop.ini 5 | 6 | # Python: 7 | *.py[cod] 8 | *.so 9 | *.egg 10 | *.egg-info 11 | dist 12 | build 13 | -------------------------------------------------------------------------------- /eegp/path/path.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | 4 | class FilePath( 5 | namedtuple('FilePath', [ 6 | 'subject', 'filetype', 'load_path', 'save_path', 'bad_channel_path' 7 | ])): 8 | __slots__ = () 9 | 10 | def __new__(cls, 11 | subject, 12 | filetype, 13 | load_path, 14 | save_path=None, 15 | bad_channel_path=None): 16 | return super(FilePath, cls).__new__(cls, subject, filetype, load_path, 17 | save_path, bad_channel_path) 18 | -------------------------------------------------------------------------------- /eegp/core/metadata.py: -------------------------------------------------------------------------------- 1 | from mne.annotations import _annotations_starts_stops 2 | 3 | 4 | def get_raw_edge_index(raw): 5 | """Returns the onset and ends index of the file splicing 6 | in the raw object. 7 | 8 | This method is very useful when using file 9 | names to define metadata. 10 | 11 | Parameters 12 | ---------- 13 | paths : mne.io.Raw 14 | Raw of one subject. 15 | 16 | Returns 17 | ------- 18 | edge_index : dict 19 | Dict contain filename and it's onset & end. 20 | i.e. edge_index = {"s1_1.cnt":(0, 40), "s1_2.cnt":(40, 80)} 21 | 22 | """ 23 | onsets, ends = _annotations_starts_stops(raw, 'edge', invert=True) 24 | edge_index = {} 25 | for path, onset, end in zip(raw.filenames, onsets, ends): 26 | edge_index[path] = (onset, end) 27 | return edge_index 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 TanTingyi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/remove_eog_mi.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from mne.datasets import eegbci 3 | from eegp.paradigms import PhysioNetMI 4 | from eegp.path import FilePath 5 | 6 | 7 | def make_filepath(dir_save, subs): 8 | filepaths = [] 9 | for i in subs: 10 | load_path = eegbci.load_data(int(i), [4, 6, 8, 10, 12, 14]) 11 | filepaths.append( 12 | FilePath(subject='s{}'.format(int(i)), 13 | filetype='edf', 14 | load_path=load_path, 15 | save_path=dir_save)) 16 | return filepaths 17 | 18 | 19 | class RemoveEOG(PhysioNetMI): 20 | def pipeline(self, filepaths): 21 | self.read_raw(filepaths) 22 | self.preprocess() 23 | self.make_epochs() 24 | self.make_data() 25 | self.save_data() 26 | 27 | 28 | if __name__ == "__main__": 29 | rm_group_eog = RemoveEOG(tmin=0., 30 | tmax=1., 31 | baseline=None, 32 | filter_low=0.5, 33 | filter_high=45., 34 | resample=128, 35 | remove_eog=True) 36 | group_size = 1 37 | sub_group = np.array_split(np.arange(1, 6), group_size) 38 | for subs in sub_group: 39 | filepaths = make_filepath(r'E:\Data\tmp\mi', subs) 40 | rm_group_eog.pipeline(filepaths) 41 | -------------------------------------------------------------------------------- /eegp/paradigms/utils.py: -------------------------------------------------------------------------------- 1 | from mne import events_from_annotations, annotations_from_events 2 | 3 | 4 | def transform_event_id(raw, transform_dic=None, description_transform=None): 5 | """Transform the description of Raw. 6 | 7 | Parameters 8 | ---------- 9 | raw : mne.Raw 10 | Raw instance. 11 | transform_dic : None | dic 12 | Dictionary holds the new id required for conversion. 13 | Which key is the old id and the value is the new id. 14 | description_transform : None | callable 15 | Function use raw as input and return new_events and new_event_id. 16 | 17 | Returns 18 | ------- 19 | None 20 | 21 | Notes 22 | ----- 23 | """ 24 | if description_transform: 25 | all_events, all_event_id = description_transform(raw) 26 | else: 27 | all_events, all_event_id = events_from_annotations(raw) 28 | 29 | if transform_dic: 30 | new_all_event_id = _transform_from_dict(all_event_id, transform_dic) 31 | else: 32 | new_all_event_id = {v: k for k, v in all_event_id.items()} 33 | 34 | annotation_new = annotations_from_events(all_events, raw.info['sfreq'], 35 | new_all_event_id) 36 | raw.set_annotations(annotation_new) 37 | 38 | 39 | def _transform_from_dict(dic1, dic2): 40 | """Transform dic1's key from dic2's value. 41 | """ 42 | dic_new = {} 43 | for key, value in dic1.items(): 44 | key_new = dic2[key] if key in dic2 else key 45 | dic_new[value] = key_new 46 | return dic_new 47 | -------------------------------------------------------------------------------- /example/delaymatch.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | 5 | from eegp.paradigms import LetterDelayMatch 6 | from eegp.path import FilePath 7 | 8 | 9 | def get_files_in_folder(dir): 10 | """返回文件夹下面所有以 s 开头,并且以 cnt 结尾的文件路径 11 | """ 12 | file_paths = list( 13 | filter(lambda x: x.endswith('cnt') and x.startswith('s'), 14 | os.listdir(dir))) 15 | file_paths = [os.path.join(dir, path) for path in file_paths] 16 | return file_paths 17 | 18 | 19 | def make_filepath(dir_load, dir_save, subs): 20 | filepaths = [] 21 | for i in subs: 22 | dir_load_sub = os.path.join(dir_load, 's' + str(i)) 23 | filepaths.append( 24 | FilePath(subject='s{}'.format(int(i)), 25 | filetype='cnt', 26 | load_path=get_files_in_folder(dir_load_sub), 27 | save_path=dir_save, 28 | bad_channel_path=os.path.join(dir_load_sub, 'bads.txt'))) 29 | return filepaths 30 | 31 | 32 | class Test(LetterDelayMatch): 33 | def pipeline(self, filepaths): 34 | self.read_raw(filepaths) 35 | self.preprocess() 36 | self.make_epochs() 37 | self.make_data() 38 | self.save_epochs() 39 | self.save_data() 40 | self.save_metadata() 41 | 42 | 43 | if __name__ == "__main__": 44 | dir_load = r'E:\Data\DelayMatch\letter' 45 | dir_save = r'E:\Data\tmp\ica_rest_250sfreq_baseline' 46 | group_size = 5 47 | sub_group = np.array_split(np.arange(1, 21), group_size) 48 | 49 | params = dict(tmin=0., 50 | tmax=5.5, 51 | baseline=(-0.5, 0), 52 | filter_low=1, 53 | filter_high=None, 54 | resample=250, 55 | remove_eog=True) 56 | 57 | for subs in sub_group: 58 | filepaths = make_filepath(dir_load, dir_save, subs) 59 | srt = Test(**params) 60 | srt.pipeline(filepaths) 61 | -------------------------------------------------------------------------------- /example/delay_srt.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | 5 | from eegp.paradigms import LetterSRT 6 | from eegp.path import FilePath 7 | 8 | 9 | def get_files_in_folder(dir): 10 | """返回文件夹下面所有以 s 开头,并且以 cnt 结尾的文件路径 11 | """ 12 | file_paths = list( 13 | filter(lambda x: x.endswith('cnt') and x.startswith('s'), 14 | os.listdir(dir))) 15 | file_paths = [os.path.join(dir, path) for path in file_paths] 16 | return file_paths 17 | 18 | 19 | def make_filepath(dir_load, dir_save, subs): 20 | filepaths = [] 21 | for i in subs: 22 | dir_load_sub = os.path.join(dir_load, 's' + str(i)) 23 | filepaths.append( 24 | FilePath(subject='s{}'.format(int(i)), 25 | filetype='cnt', 26 | load_path=get_files_in_folder(dir_load_sub), 27 | save_path=dir_save, 28 | bad_channel_path=os.path.join(dir_load_sub, 'bads.txt'))) 29 | return filepaths 30 | 31 | 32 | class Test(LetterSRT): 33 | def pipeline(self, filepaths): 34 | self.read_raw(filepaths) 35 | self.preprocess() 36 | self.make_epochs() 37 | self.make_data() 38 | self.save_data() 39 | self.save_metadata() 40 | 41 | 42 | if __name__ == "__main__": 43 | # TODO 仔细看一些打标错误trial的数据 44 | dir_load = r'E:\Data\DelayMatch\letter' 45 | dir_save = r'E:\Data\tmp\srt_normal_long_bins_reject500' 46 | group_size = 20 47 | sub_group = np.array_split(np.arange(1, 21), group_size) 48 | 49 | params = dict(tmin=0., 50 | tmax=5., 51 | baseline=None, 52 | filter_low=0.5, 53 | filter_high=None, 54 | resample=160, 55 | remove_eog=False) 56 | 57 | for subs in sub_group: 58 | filepaths = make_filepath(dir_load, dir_save, subs) 59 | srt = Test(**params) 60 | srt.pipeline(filepaths) 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EEGP 2 | Easy-to-use EEG preprocessing pipeline based on [MNE][] 3 | 4 | This project can help you build an EEG data process pipeline that is easy to share. 5 | 6 | ## Background 7 | For beginners, in order to preprocess EEG data, there is a lot of knowledge to know. At the same time, the pre-processing process is complicated, and even oneself may forget the details involved in preprocessing. This increases the difficulty for researchers to share scientific research results, and also makes it difficult to reproduce experimental results. 8 | 9 | By using a unified EEG data preprocessing pipeline, it is convenient for researchers to share their research results, and experimental results are more easily reproduced. At the same time, beginners can quickly get started by reading the preprocessing code of others. 10 | 11 | ## Requirements 12 | mne 13 | pandas 14 | scikit-learn 15 | numpy 16 | 17 | ## Usage 18 | You can refer to the implemented classes in paradigms folder to create preprocessing pipeline of your own. 19 | When you have written your own class, others only need the following lines of code to reproduce your experimental results. 20 | 21 | ```python 22 | from eegp.paradigms import MIFeetHand 23 | from eegp.path import FilePath 24 | 25 | class SaveData(MIFeetHand): 26 | def pipeline(self, filepaths): 27 | self.read_raw(filepaths) 28 | self.preprocess() 29 | self.make_epochs() 30 | self.make_data() 31 | self.save_data() 32 | 33 | filepath = FilePath(subject='S1', 34 | filetype='edf', 35 | load_path='/tmp/data/S001R04.edf', 36 | save_path='/tmp/data') 37 | savedata = SaveData() 38 | savedata.pipeline(filepaths) 39 | ``` 40 | 41 | ## Demo 42 | This is an [example][] that can be run online to let you know how easy it is to use when you share your pipeline with others. 43 | 44 | ## ToDo List 45 | 46 | 47 | 48 | [MNE]:https://mne.tools/stable/index.html "MNE" 49 | [example]:https://colab.research.google.com/drive/19hgfVbP47Ib-JUq4dyVxy77_INOAkSIG?usp=sharing "example" 50 | -------------------------------------------------------------------------------- /eegp/path/utils.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from os.path import isfile 3 | from pathlib import Path 4 | 5 | 6 | def _depth_count(x): 7 | """Return maximum depth of the returned list. 8 | 9 | Parameters 10 | ---------- 11 | x : list. 12 | 13 | Return 14 | ------ 15 | ans : int 16 | maximum depth. 17 | """ 18 | return int(isinstance(x, 19 | list)) and len(x) and 1 + max(map(_depth_count, x)) 20 | 21 | 22 | def check_paths(paths): 23 | """Return path list make sure load_path is a list and all paths exit. 24 | 25 | Parameters 26 | ---------- 27 | paths : FilePath | list of FilePath. 28 | FilePath object to be detected. 29 | 30 | Return 31 | ------ 32 | paths_checked : list. 33 | List of FilePaths that conform to the specification 34 | """ 35 | 36 | if _depth_count(paths) < 1: 37 | paths_checked = [paths] 38 | else: 39 | paths_checked = deepcopy(paths) 40 | for path in paths_checked: 41 | if not isinstance(path.load_path, list): 42 | path.load_path = [path.load_path] 43 | 44 | file_uncheck = path.load_path.copy() 45 | if path.bad_channel_path: 46 | file_uncheck.append(path.bad_channel_path) 47 | _files_not_exist = _find_path_not_exist(file_uncheck) 48 | 49 | if _files_not_exist: 50 | raise RuntimeError('These files do not exit:', *_files_not_exist) 51 | return paths_checked 52 | 53 | 54 | def _find_path_not_exist(paths): 55 | """Recursively check whether the path in the list exists. 56 | 57 | takes a list as input and return path if it is not exist. 58 | 59 | Parameters 60 | ---------- 61 | paths : list. 62 | list containing paths. 63 | 64 | Return 65 | ------ 66 | paths_not_exist : list 67 | paths which not exist. 68 | """ 69 | paths_not_exist = [] 70 | 71 | def _check_file(paths): 72 | if not isinstance(paths, list): 73 | if not isfile(paths): 74 | return paths 75 | else: 76 | for path in paths: 77 | ans = _check_file(path) 78 | if isinstance(ans, str): 79 | paths_not_exist.append(ans) 80 | 81 | _check_file(paths) 82 | return paths_not_exist 83 | 84 | 85 | def create_dir(path): 86 | Path(path).mkdir(parents=True, exist_ok=True) 87 | -------------------------------------------------------------------------------- /eegp/core/load.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | 4 | import mne 5 | import numpy as np 6 | import scipy.io as scio 7 | 8 | from mne import Annotations 9 | from mne.io import concatenate_raws, read_raw_cnt, read_raw_edf, read_raw_bdf 10 | 11 | 12 | def _read_annotations_bdf(annotations): 13 | pat = '([+-]\\d+\\.?\\d*)(\x15(\\d+\\.?\\d*))?(\x14.*?)\x14\x00' 14 | if isinstance(annotations, str): 15 | with open(annotations, encoding='latin-1') as annot_file: 16 | triggers = re.findall(pat, annot_file.read()) 17 | else: 18 | tals = bytearray() 19 | for chan in annotations: 20 | this_chan = chan.ravel() 21 | if this_chan.dtype == np.int32: # BDF 22 | this_chan.dtype = np.uint8 23 | this_chan = this_chan.reshape(-1, 4) 24 | # Why only keep the first 3 bytes as BDF values 25 | # are stored with 24 bits (not 32) 26 | this_chan = this_chan[:, :3].ravel() 27 | for s in this_chan: 28 | tals.extend(s) 29 | else: 30 | for s in this_chan: 31 | i = int(s) 32 | tals.extend(np.uint8([i % 256, i // 256])) 33 | 34 | # use of latin-1 because characters are only encoded for the first 256 35 | # code points and utf-8 can triggers an "invalid continuation byte" 36 | # error 37 | triggers = re.findall(pat, tals.decode('latin-1')) 38 | 39 | events = [] 40 | for ev in triggers: 41 | onset = float(ev[0]) 42 | duration = float(ev[2]) if ev[2] else 0 43 | for description in ev[3].split('\x14')[1:]: 44 | if description: 45 | events.append([onset, duration, description]) 46 | return zip(*events) if events else (list(), list(), list()) 47 | 48 | 49 | def read_raw_brk(path, **kwargs): 50 | """Read function for Neuracle Technology. 51 | 52 | Parameters 53 | ---------- 54 | path : str 55 | Path to the data.bdf. 56 | 57 | Returns 58 | ------- 59 | raw : instance of RawBDF. 60 | The raw data. 61 | 62 | Notes 63 | ----- 64 | data.bdf and evt.bdf must be in the same path. 65 | """ 66 | raw = read_raw_bdf(path, **kwargs) 67 | dic, _ = os.path.split(path) 68 | annotation = read_raw_bdf(os.path.join(dic, 'evt.bdf'), **kwargs) 69 | 70 | idx = np.empty(0, int) 71 | tal_data = annotation._read_segment_file(np.empty( 72 | (0, annotation.n_times)), idx, 0, 0, int(annotation.n_times), 73 | np.ones((len(idx), 1)), None) 74 | onset, duration, description = _read_annotations_bdf(tal_data[0]) 75 | onset = np.array([i for i in onset], dtype=np.float64) 76 | duration = np.array([int(i) for i in duration], dtype=np.int64) 77 | desc = np.array([int(i) for i in description], dtype=np.int64) 78 | annotation_new = Annotations(onset, duration, desc) 79 | raw.set_annotations(annotation_new) 80 | return raw 81 | 82 | 83 | def _read_mat(filepath): 84 | mat = scio.loadmat(filepath) 85 | ch_names = [ 86 | 'Fp1', 'Fp2', 'F3', 'F4', 'C3', 'C4', 'P3', 'P4', 'O1', 'O2', 'F7', 87 | 'F8', 'T3', 'T4', 'T5', 'T6' 88 | ] 89 | sfreq = 1000 90 | data = mat['eegdata'][0][0][1] * 1e-6 91 | onset = mat['eegdata'][0][0][4].squeeze() 92 | onset = [sample / sfreq for sample in onset] 93 | duration = np.zeros_like(onset) 94 | description = np.array(['onset'] * len(onset)) 95 | 96 | info = mne.create_info(ch_names, sfreq, 'eeg') 97 | raw = mne.io.RawArray(data, info) 98 | annotations = Annotations(onset, duration, description) 99 | raw.set_annotations(annotations) 100 | return raw 101 | 102 | 103 | def _read_func(filetype, filepath): 104 | if filetype == 'cnt': 105 | return read_raw_cnt(filepath, preload=True, verbose=0) 106 | elif filetype == 'edf': 107 | return read_raw_edf(filepath, preload=True, verbose=0) 108 | elif filetype == 'bdf': 109 | return read_raw_bdf(filepath, preload=True, verbose=0) 110 | elif filetype == 'brk': 111 | return read_raw_brk(filepath, preload=True, verbose=0) 112 | 113 | 114 | def read_raw(paths): 115 | """Return raws list from paths. 116 | 117 | Parameters 118 | ---------- 119 | paths : list of FilePath. 120 | List includes the FilePath of all subjects. 121 | 122 | Returns 123 | ------- 124 | raws : list. 125 | List includes the raw of all subjects. 126 | 127 | Notes 128 | ----- 129 | """ 130 | raws = [] 131 | for subject_path in paths: 132 | raw = concatenate_raws([ 133 | _read_func(subject_path.filetype, f) 134 | for f in subject_path.load_path 135 | ]) 136 | if subject_path.bad_channel_path: 137 | with open(subject_path.bad_channel_path, 'r') as f: 138 | bad_channel = f.read().split() 139 | raw.info['bads'] = bad_channel 140 | raws.append(raw) 141 | return raws 142 | -------------------------------------------------------------------------------- /eegp/core/preprocess.py: -------------------------------------------------------------------------------- 1 | from mne import make_sphere_model, setup_volume_source_space, make_forward_solution 2 | from mne.channels import make_standard_montage 3 | from mne.preprocessing import ICA, corrmap 4 | 5 | 6 | def channel_repair_exclud(raws, exclude, montage): 7 | """Remove unnecessary channels and repair bad sectors. 8 | 9 | Parameters 10 | ---------- 11 | raws : list of Raw. 12 | The raw data. 13 | exclude : list 14 | List of channel name. 15 | montage : str 16 | Multiple channel montage supported by MNE. 17 | 18 | Returns 19 | ------- 20 | raws : list of Raw. 21 | The raw data. 22 | 23 | Notes 24 | ----- 25 | """ 26 | if not isinstance(raws, list): 27 | raws = [raws] 28 | for raw in raws: 29 | raw.pick(picks='eeg', exclude=exclude) 30 | raw.set_montage(make_standard_montage(montage), match_case=False) 31 | raw.interpolate_bads() 32 | return raws 33 | 34 | 35 | def remove_eog_ica(raw, n_components, ch_name, threshold): 36 | """Remove EOG artifacts by ICA. 37 | 38 | Parameters 39 | ---------- 40 | raw : instance of Raw. 41 | The raw data. 42 | n_components : int 43 | Number of principal components for ICA. 44 | ch_name : str 45 | The name of the channel to use for EOG peak detection. 46 | threshold : int 47 | The value above which a feature is classified as outlier. 48 | 49 | Returns 50 | ------- 51 | raw : instance of Raw. 52 | The raw data. 53 | 54 | Notes 55 | ----- 56 | """ 57 | ica = ICA(n_components=n_components, max_iter='auto') 58 | ica.fit(raw, verbose=0) 59 | while threshold > 1: 60 | eog_inds, _ = ica.find_bads_eog(raw, 61 | ch_name=ch_name, 62 | threshold=threshold, 63 | verbose=0) 64 | if eog_inds: 65 | break 66 | threshold -= 0.3 67 | 68 | if not eog_inds: 69 | raise RuntimeError('Didn\'t find a EOG component.') 70 | 71 | ica.plot_properties(raw, eog_inds) 72 | ica.exclude = eog_inds 73 | ica.apply(raw, verbose=0) 74 | return raw 75 | 76 | 77 | def remove_eog_template_ica(raws, n_components, ch_name, threshold): 78 | """Remove EOG artifacts by similar Independent Components across subjects. 79 | 80 | Parameters 81 | ---------- 82 | raws : list 83 | List Raw instances. 84 | n_components : int 85 | Number of principal components for ICA. 86 | ch_name : str 87 | The name of the channel to use for EOG peak detection. 88 | threshold : int 89 | The value above which a feature is classified as outlier. 90 | 91 | Returns 92 | ------- 93 | raws : list 94 | List Raw instances. 95 | 96 | Notes 97 | ----- 98 | """ 99 | icas = [ 100 | ICA(n_components=n_components, max_iter='auto').copy().fit(raw, 101 | verbose=0) 102 | for raw in raws 103 | ] 104 | for raw, ica in zip(raws, icas): 105 | eog_inds, _ = ica.find_bads_eog(raw, ch_name=ch_name, verbose=0) 106 | if eog_inds: 107 | break 108 | if not eog_inds: 109 | raise RuntimeError( 110 | 'This group of subjects does not have an EOG template, add more subjects.' 111 | ) 112 | 113 | _ = corrmap(icas, 114 | template=(0, eog_inds[0]), 115 | threshold=threshold, 116 | label='blink') 117 | for raw, ica in zip(raws, icas): 118 | ica.exclude = ica.labels_['blink'] 119 | ica.apply(raw, verbose=0) 120 | return raws 121 | 122 | 123 | def rest_reference(raws): 124 | """Re-reference the data by REST. 125 | 126 | Parameters 127 | ---------- 128 | raws : list of Raw. 129 | The raw data. 130 | 131 | Returns 132 | ------- 133 | raws : list of Raw. 134 | The raw data. 135 | 136 | Notes 137 | ----- 138 | """ 139 | if not isinstance(raws, list): 140 | raws = [raws] 141 | for raw in raws: 142 | sphere = make_sphere_model('auto', 'auto', raw.info, verbose=0) 143 | src = setup_volume_source_space(sphere=sphere, 144 | exclude=30., 145 | pos=15., 146 | verbose=0) 147 | forward = make_forward_solution(raw.info, 148 | trans=None, 149 | src=src, 150 | bem=sphere, 151 | verbose=0) 152 | raw.set_eeg_reference('REST', forward=forward) 153 | return raws 154 | 155 | 156 | def band_pass_filter(raws, low, high): 157 | """Bandpass filter. 158 | 159 | Parameters 160 | ---------- 161 | raws : list of Raw. 162 | The raw data. 163 | 164 | Returns 165 | ------- 166 | raws : list of Raw. 167 | The raw data. 168 | 169 | Notes 170 | ----- 171 | """ 172 | if not isinstance(raws, list): 173 | raws = [raws] 174 | for raw in raws: 175 | raw.filter(low, 176 | high, 177 | skip_by_annotation='edge', 178 | method='iir', 179 | verbose=0) 180 | return raws 181 | -------------------------------------------------------------------------------- /eegp/paradigms/delaymatch.py: -------------------------------------------------------------------------------- 1 | """ 2 | Paradigm: Letter delay match 2017 3 | Paper: A Fusion Feature for Enhancing the Performance 4 | of Classification in Working Memory Load 5 | With Single-Trial Detection 6 | DOI: 10.1109/TNSRE.2019.2936997 7 | Authors: Matthew Tan <5636374@qq.com> 8 | Update time: 2021.4.20 9 | """ 10 | 11 | import numpy as np 12 | import pandas as pd 13 | 14 | from mne import Epochs, events_from_annotations 15 | from mne.epochs import make_metadata 16 | 17 | from .base import BaseParadigm 18 | from .utils import transform_event_id 19 | from ..path import check_paths 20 | from ..core.load import read_raw 21 | from ..core.preprocess import (remove_eog_template_ica, remove_eog_ica, 22 | channel_repair_exclud, rest_reference, 23 | band_pass_filter) 24 | 25 | 26 | class LetterDelayMatch(BaseParadigm): 27 | """This class provides preprocess pipeline. 28 | """ 29 | def __init__(self, 30 | tmin, 31 | tmax, 32 | filter_low=1, 33 | filter_high=None, 34 | resample=250, 35 | baseline=None, 36 | remove_eog=False): 37 | """ 38 | Parameters 39 | ---------- 40 | tmin : int | float 41 | Start time before event. If nothing is provided, 42 | defaults to -0.2. 43 | tmax : int | float 44 | End time after event. If nothing is provided, 45 | defaults to 0.5. 46 | filter_low : None | int | float 47 | For FIR filters, the lower pass-band edge; 48 | If None the data are only low-passed. 49 | filter_high : None | int | float 50 | For FIR filters, the upper pass-band edge; 51 | If None the data are only high-passed. 52 | resample : int 53 | New sample rate to use. 54 | baseline : None | tuple 55 | The time interval to consider as “baseline” when 56 | applying baseline correction. If None, do not apply 57 | baseline correction. 58 | remove_eog : bool 59 | Whether to remove EOG artifacts. 60 | 61 | """ 62 | super(LetterDelayMatch, self).__init__(code='Delay Match Letter') 63 | self.tmin = tmin 64 | self.tmax = tmax 65 | self.filter_low = filter_low 66 | self.filter_high = filter_high 67 | self.resample = resample 68 | self.baseline = baseline 69 | self.remove_eog = remove_eog 70 | 71 | def read_raw(self, paths): 72 | self._paths = check_paths(paths) 73 | self._raws = read_raw(self._paths) 74 | 75 | def preprocess(self): 76 | if not self._raws: 77 | raise RuntimeError('File not Loaded.') 78 | self._raws = channel_repair_exclud( 79 | self._raws, 80 | exclude=['CB1', 'CB2', 'HEO', 'VEO', 'M1', 'M2'], 81 | montage='standard_1020') 82 | self._raws = band_pass_filter(self._raws, self.filter_low, 83 | self.filter_high) 84 | if self.remove_eog: 85 | self._raws = self._remove_eog(self._raws) 86 | self._raws = rest_reference(self._raws) 87 | 88 | def make_epochs(self): 89 | if not self._raws: 90 | raise RuntimeError( 91 | 'File haven\'t loaded yet, please load file first.') 92 | self._epochs = [] 93 | for raw in self._raws: 94 | events, event_id, metadata = self._metadata_from_raw(raw) 95 | epochs = Epochs(raw, 96 | events, 97 | event_id, 98 | self.tmin - 0.5, 99 | self.tmax + 0.2, 100 | baseline=self.baseline, 101 | metadata=metadata, 102 | preload=True) 103 | 104 | epochs = self._filter_epochs(epochs) 105 | epochs.metadata = self._make_metadata(epochs.metadata) 106 | self._epochs.append(epochs.resample(self.resample)) 107 | 108 | def make_data(self): 109 | if not self.epochs: 110 | self.make_epochs() 111 | 112 | self._datas = [] 113 | for epoch in self.epochs: 114 | self._datas.append(epoch.copy().crop( 115 | tmin=self.tmin, tmax=self.tmax, 116 | include_tmax=False).get_data().astype(np.float32)) 117 | 118 | def _transform_event_id(self, raw): 119 | transform_dic = { 120 | '22': 'stimulus/memory/item_2', 121 | '44': 'stimulus/memory/item_4', 122 | '88': 'stimulus/memory/item_8', 123 | '122': 'stimulus/test/item_2', 124 | '144': 'stimulus/test/item_4', 125 | '188': 'stimulus/test/item_8', 126 | '1': 'response/correct', 127 | '3': 'response/wrong' 128 | } 129 | transform_event_id(raw, transform_dic) 130 | 131 | def _remove_eog(self, raws): 132 | if len(raws) > 1: 133 | raws = remove_eog_template_ica(raws, 134 | n_components=15, 135 | ch_name='FPZ', 136 | threshold=0.8) 137 | else: 138 | raws = [ 139 | remove_eog_ica(raws[0], 140 | n_components=15, 141 | ch_name='FPZ', 142 | threshold=3) 143 | ] 144 | return raws 145 | 146 | def _metadata_from_raw(self, raw): 147 | """...22---------------122-----------------1 or 3... 148 | ...| maintenance | reaction time |... 149 | """ 150 | self._transform_event_id(raw) 151 | all_events, all_event_id = events_from_annotations(raw) 152 | row_events = [ 153 | 'stimulus/memory/item_2', 154 | 'stimulus/memory/item_4', 155 | 'stimulus/memory/item_8', 156 | ] 157 | keep_first = ['stimulus', 'test', 'response'] 158 | metadata_tmin, metadata_tmax = 0.0, 5.5 159 | 160 | metadata, events, event_id = make_metadata(events=all_events, 161 | event_id=all_event_id, 162 | tmin=metadata_tmin, 163 | tmax=metadata_tmax, 164 | sfreq=raw.info['sfreq'], 165 | row_events=row_events, 166 | keep_first=keep_first) 167 | # all times of the time-locked events should be zero 168 | assert all(metadata['stimulus'] == 0) 169 | return events, event_id, metadata 170 | 171 | def _make_metadata(self, metadata): 172 | metadata['response_time'] = metadata['response'] - metadata['test'] 173 | # add srt 174 | reaction_time_max = 1.617 175 | 176 | def srt(x): 177 | if x['first_response'] == 'correct': 178 | return reaction_time_max - x['response_time'] 179 | else: 180 | return x['response_time'] - reaction_time_max 181 | 182 | metadata['srt'] = metadata.apply(srt, axis=1) 183 | metadata['srt_bins_freq'] = pd.qcut(metadata['srt'], 184 | 3, 185 | labels=[0, 1, 2]) 186 | bins = 3 187 | bins_edges = np.linspace(-reaction_time_max, reaction_time_max, 188 | bins + 1) 189 | metadata['srt_distribution_3'] = pd.cut(metadata['srt'], 190 | bins_edges, 191 | labels=[0, 1, 2]) 192 | return metadata 193 | -------------------------------------------------------------------------------- /eegp/paradigms/physionet.py: -------------------------------------------------------------------------------- 1 | """ 2 | Paradigm: PhysioNet MI 3 | Website: https://physionet.org/content/eegmmidb/1.0.0/ 4 | Authors: Matthew Tan <5636374@qq.com> 5 | Update time: 2021.4.20 6 | """ 7 | import os 8 | 9 | import numpy as np 10 | 11 | from mne import Epochs, events_from_annotations 12 | from mne.datasets import eegbci 13 | from mne.epochs import make_metadata 14 | 15 | from .base import BaseParadigm 16 | from .utils import transform_event_id 17 | from ..path import check_paths 18 | from ..core.load import read_raw 19 | from ..core.preprocess import (remove_eog_template_ica, remove_eog_ica, 20 | channel_repair_exclud, band_pass_filter) 21 | from ..core.metadata import get_raw_edge_index 22 | 23 | 24 | class PhysioNetMI(BaseParadigm): 25 | """This class provides preprocess pipeline. 26 | """ 27 | def __init__(self, 28 | tmin, 29 | tmax, 30 | filter_low=7., 31 | filter_high=30., 32 | resample=160, 33 | baseline=None, 34 | remove_eog=False): 35 | """ 36 | Parameters 37 | ---------- 38 | tmin : int | float 39 | Start time before event. If nothing is provided, 40 | defaults to -0.2. 41 | tmax : int | float 42 | End time after event. If nothing is provided, 43 | defaults to 0.5. 44 | filter_low : None | int | float 45 | For FIR filters, the lower pass-band edge; 46 | If None the data are only low-passed. 47 | filter_high : None | int | float 48 | For FIR filters, the upper pass-band edge; 49 | If None the data are only high-passed. 50 | resample : int 51 | New sample rate to use. 52 | baseline : None | tuple 53 | The time interval to consider as “baseline” when 54 | applying baseline correction. If None, do not apply 55 | baseline correction. 56 | remove_eog : bool 57 | Whether to remove EOG artifacts. 58 | 59 | """ 60 | super(PhysioNetMI, self).__init__(code='PhysioNet-MI') 61 | self.tmin = tmin 62 | self.tmax = tmax 63 | self.filter_low = filter_low 64 | self.filter_high = filter_high 65 | self.resample = resample 66 | self.baseline = baseline 67 | self.remove_eog = remove_eog 68 | 69 | def read_raw(self, paths): 70 | self._paths = check_paths(paths) 71 | self._raws = read_raw(self._paths) 72 | 73 | def preprocess(self): 74 | if not self._raws: 75 | raise RuntimeError('File not Loaded.') 76 | 77 | for raw in self._raws: 78 | eegbci.standardize(raw) # set channel names 79 | # strip channel names of "." characters 80 | self._raws = channel_repair_exclud(self._raws, 81 | exclude='bads', 82 | montage='standard_1005') 83 | self._raws = band_pass_filter(self._raws, self.filter_low, 84 | self.filter_high) 85 | if self.remove_eog: 86 | self._raws = self._remove_eog(self._raws) 87 | 88 | def make_epochs(self): 89 | if not self._raws: 90 | raise RuntimeError( 91 | 'File haven\'t loaded yet, please load file first.') 92 | self._epochs = [] 93 | for raw in self._raws: 94 | events, event_id, metadata = self._metadata_from_raw(raw) 95 | epochs = Epochs(raw, 96 | events, 97 | event_id, 98 | self.tmin - 0.5, 99 | self.tmax + 0.2, 100 | baseline=self.baseline, 101 | metadata=metadata, 102 | preload=True) 103 | 104 | epochs = self._filter_epochs(epochs) 105 | epochs.metadata = self._make_metadata(epochs.metadata) 106 | self._epochs.append(epochs.resample(self.resample)) 107 | 108 | def _remove_eog(self, raws): 109 | if len(raws) > 1: 110 | raws = remove_eog_template_ica(raws, 111 | n_components=15, 112 | ch_name='Fpz', 113 | threshold=0.8) 114 | else: 115 | raws = [ 116 | remove_eog_ica(raws[0], 117 | n_components=15, 118 | ch_name='Fpz', 119 | threshold=3) 120 | ] 121 | return raws 122 | 123 | def _metadata_from_raw(self, raw): 124 | self._transform_event_id(raw) 125 | all_events, all_event_id = events_from_annotations(raw) 126 | 127 | row_events = [ 128 | 'stimulus/imagine/hands/all', 'stimulus/imagine/feet/all', 129 | 'stimulus/imagine/hands/left', 'stimulus/imagine/hands/right' 130 | ] 131 | keep_first = ['stimulus'] 132 | metadata_tmin, metadata_tmax = 0.0, 4 133 | 134 | metadata, events, event_id = make_metadata(events=all_events, 135 | event_id=all_event_id, 136 | tmin=metadata_tmin, 137 | tmax=metadata_tmax, 138 | sfreq=raw.info['sfreq'], 139 | row_events=row_events, 140 | keep_first=keep_first) 141 | # all times of the time-locked events should be zero 142 | assert all(metadata['stimulus'] == 0) 143 | return events, event_id, metadata 144 | 145 | def _transform_event_id(self, raw): 146 | def description_transform(raw): 147 | new_event_id = { 148 | 'rest': 0, 149 | 'stimulus/imagine/hands/all': 1, 150 | 'stimulus/imagine/feet/all': 2, 151 | 'stimulus/imagine/hands/left': 3, 152 | 'stimulus/imagine/hands/right': 4 153 | } 154 | all_events, all_event_id = events_from_annotations(raw) 155 | file_edges = get_raw_edge_index(raw) 156 | 157 | # help function 158 | def index_2_run(index): 159 | for filename, (onset, end) in file_edges.items(): 160 | if onset <= index <= end: 161 | run = int(os.path.split(filename)[1].split('R')[1][:2]) 162 | return run 163 | 164 | def description_transform_by_run(run, old_description): 165 | runs_id = { 166 | 'hands&feet': set([6, 10, 14]), 167 | 'left&right': set([4, 8, 12]) 168 | } 169 | for run_type, runs in runs_id.items(): 170 | if run in runs: 171 | if run_type == 'hands&feet': 172 | if old_description == all_event_id['T1']: 173 | return 1 174 | elif old_description == all_event_id['T2']: 175 | return 2 176 | elif old_description == all_event_id['T0']: 177 | return 0 178 | elif run_type == 'left&right': 179 | if old_description == all_event_id['T1']: 180 | return 3 181 | elif old_description == all_event_id['T2']: 182 | return 4 183 | elif old_description == all_event_id['T0']: 184 | return 0 185 | 186 | def mapfunc(event): 187 | run = index_2_run(event[0]) 188 | new_description = description_transform_by_run(run, event[2]) 189 | return [event[0], event[1], new_description] 190 | 191 | new_events = np.array(list(map(mapfunc, all_events))) 192 | return new_events, new_event_id 193 | 194 | transform_event_id(raw, description_transform=description_transform) 195 | 196 | def make_data(self): 197 | if not self.epochs: 198 | self.make_epochs() 199 | 200 | self._datas = [] 201 | for epoch in self.epochs: 202 | self._datas.append(epoch.copy().crop( 203 | tmin=self.tmin, tmax=self.tmax, 204 | include_tmax=False).get_data().astype(np.float32)) 205 | -------------------------------------------------------------------------------- /eegp/paradigms/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Bast class of paradigm. 3 | 4 | For each paradigm, there should be a unified preprocessing process. 5 | Depending on the classification task, the same paradigm may has different 6 | label definition rule. This means that two classes should be implemented, 7 | where the parent class defines a unified preprocessing pipeline and adds metadata, 8 | such as the response time of each trial, the type of stimulus, and so on. 9 | The sub-category defines the label of the current classification task. 10 | 11 | Authors: Matthew Tan <5636374@qq.com> 12 | Time: 2021.4.15 13 | """ 14 | import abc 15 | import os 16 | from copy import deepcopy 17 | 18 | import numpy as np 19 | 20 | from ..path import create_dir 21 | 22 | 23 | class BaseParadigm(metaclass=abc.ABCMeta): 24 | """Abstract base class for paradigm-type classes. 25 | 26 | This class provides basic functionality and should 27 | never be instantiated directly. 28 | """ 29 | def __init__(self, code): 30 | """ 31 | Parameters 32 | ---------- 33 | code : string 34 | Unicode of paradigm. 35 | 36 | Attributes 37 | ---------- 38 | self.raws : list 39 | List of Raw for each subject. 40 | self.epochs : list 41 | List of Epoch for each subject. 42 | self.paths : list 43 | List of FilePath for each subject. 44 | self.datas : list 45 | List of data for each subject. 46 | self.labels : list 47 | List of label for each subject. 48 | """ 49 | self.code = code 50 | self._raws = [] 51 | self._epochs = [] 52 | self._paths = [] 53 | self._datas = [] 54 | 55 | @property 56 | def raws(self): 57 | return deepcopy(self._raws) 58 | 59 | @property 60 | def epochs(self): 61 | return deepcopy(self._epochs) 62 | 63 | @property 64 | def paths(self): 65 | return deepcopy(self._paths) 66 | 67 | @property 68 | def datas(self): 69 | return deepcopy(self._datas) 70 | 71 | @abc.abstractmethod 72 | def read_raw(self, paths): 73 | """Load raw file from FilePath. 74 | 75 | Support to load only one subject or multiple subjects 76 | at the same time. 77 | 78 | 79 | Parameters 80 | ---------- 81 | paths : FilePath | list 82 | FilePath that keep the load path and save path. 83 | 84 | Returns 85 | ------- 86 | None 87 | 88 | Notes 89 | ----- 90 | Loading data of multiple subjects at the same time 91 | is suitable for the situation where you want to run ica 92 | to remove the eye by template matching method, 93 | but should be careful that it will take up a lot of memory. 94 | """ 95 | 96 | pass 97 | 98 | @abc.abstractmethod 99 | def preprocess(self): 100 | """Preprocess raw data. 101 | 102 | Parameters 103 | ---------- 104 | None 105 | 106 | Returns 107 | ------- 108 | None 109 | 110 | Notes 111 | ----- 112 | You should implement preprocessing operations for 113 | raw data in this method, such as bad channel marking 114 | and repair, band-pass filtering and ICA to remove 115 | EOG artifacts. 116 | 117 | Examples 118 | -------- 119 | 120 | >>> for raw in self.__raws: 121 | >>> raw = channel_repair_exclud( 122 | >>> raw, 123 | >>> exclude=['HEO', 'VEO'], 124 | >>> montage='standard_1020') 125 | >>> raw.filter(self.filter_low, 126 | >>> self.filter_high, 127 | >>> skip_by_annotation='edge') 128 | """ 129 | pass 130 | 131 | @abc.abstractmethod 132 | def make_epochs(self): 133 | """Make epochs from raw. 134 | 135 | Parameters 136 | ---------- 137 | None 138 | 139 | Returns 140 | ------- 141 | None 142 | 143 | Notes 144 | ----- 145 | You should split the raw data into epochs in this method, 146 | and add meta information for the epochs if necessary. 147 | You can also implement the self._filter_epochs() method 148 | to keep epochs you need. 149 | Examples 150 | -------- 151 | >>> self.__epochs = [] 152 | >>> for raw in self.__raws: 153 | >>> events, event_id = self._define_trials(raw) 154 | >>> epochs = Epochs(raw, events, event_id) 155 | >>> epochs.metadata = self._metadata_from_raw(epochs, raw) 156 | >>> epochs = self._filter_epochs(epochs) 157 | >>> epochs.metadata = self._make_metadata(epochs.metadata) 158 | >>> self.__epochs.append(epochs.resample(self.resample)) 159 | 160 | """ 161 | pass 162 | 163 | @abc.abstractmethod 164 | def pipeline(self, *args, **kwargs): 165 | """The main entrance of data processing. 166 | 167 | Notes 168 | ----- 169 | This method should be user-defined. 170 | 171 | Examples 172 | -------- 173 | 174 | >>> self.read_raw(filepaths) 175 | >>> self.preprocess() 176 | >>> self.make_epochs() 177 | >>> self.make_data() 178 | >>> self.save_data() 179 | """ 180 | pass 181 | 182 | def make_data(self): 183 | """Make data and label. 184 | 185 | Parameters 186 | ---------- 187 | None 188 | 189 | Returns 190 | ------- 191 | None 192 | 193 | Notes 194 | ----- 195 | 196 | Should only be implemented in subclasses of the paradigm class, 197 | used to define different types of labels in the same paradigm. 198 | 199 | """ 200 | pass 201 | 202 | def save_raw(self, **kwargs): 203 | """Save raw file and return save path. 204 | 205 | Parameters 206 | ---------- 207 | kwargs : kwargs for Raw.save(). 208 | 209 | Returns 210 | ------- 211 | save_paths : list 212 | List containing the save path. 213 | 214 | Notes 215 | ----- 216 | """ 217 | save_paths = [] 218 | for raw, path in zip(self.raws, self.paths): 219 | try: 220 | raw_path = os.path.join(path.save_path, 'raw') 221 | except TypeError: 222 | print('No save directory specified for subject {}!'.format( 223 | path.subject)) 224 | create_dir(raw_path) 225 | save_path = os.path.join(raw_path, path.subject) + '_raw.fif' 226 | raw.save(save_path, **kwargs) 227 | save_paths.append(save_path) 228 | return save_paths 229 | 230 | def save_epochs(self, **kwargs): 231 | """Save epochs file and return save path. 232 | 233 | Parameters 234 | ---------- 235 | kwargs : kwargs for Epoch.save(). 236 | 237 | Returns 238 | ------- 239 | save_paths : list 240 | List containing the save path. 241 | 242 | Notes 243 | ----- 244 | """ 245 | save_paths = [] 246 | for epochs, path in zip(self.epochs, self.paths): 247 | try: 248 | epoch_path = os.path.join(path.save_path, 'epochs') 249 | except TypeError: 250 | print('No save directory specified for subject {}!'.format( 251 | path.subject)) 252 | create_dir(epoch_path) 253 | save_path = os.path.join(epoch_path, path.subject) + '_epo.fif' 254 | epochs.save(save_path, **kwargs) 255 | save_paths.append(save_path) 256 | 257 | def save_data(self): 258 | """Save data and label and return save path. 259 | 260 | Parameters 261 | ---------- 262 | None 263 | 264 | Returns 265 | ------- 266 | save_paths : list 267 | List containing the save path. 268 | 269 | Notes 270 | ----- 271 | The saved file can be read by np.load() 272 | >>> with np.load(file) as f: 273 | >>> data = f['data'] 274 | """ 275 | save_paths = [] 276 | for data, path in zip(self._datas, self.paths): 277 | try: 278 | data_path = os.path.join(path.save_path, 'data') 279 | except TypeError: 280 | print('No save directory specified for subject {}!'.format( 281 | path.subject)) 282 | create_dir(data_path) 283 | save_path = os.path.join(data_path, path.subject) 284 | np.save(save_path, data) 285 | save_paths.append(save_path) 286 | return save_paths 287 | 288 | def save_metadata(self, **kwargs): 289 | """Save metadata to .csv and return save path. 290 | 291 | Parameters 292 | ---------- 293 | kwargs : kwargs for DataFrame.to_csv(). 294 | 295 | Returns 296 | ------- 297 | save_paths : list 298 | List containing the save path. 299 | 300 | Notes 301 | ----- 302 | """ 303 | save_paths = [] 304 | for epoch, path in zip(self.epochs, self.paths): 305 | try: 306 | metadata_path = os.path.join(path.save_path, 'metadata') 307 | except TypeError: 308 | print('No save directory specified for subject {}!'.format( 309 | path.subject)) 310 | create_dir(metadata_path) 311 | save_path = os.path.join(metadata_path, path.subject) + '.csv' 312 | epoch.metadata.to_csv(save_path, **kwargs) 313 | save_paths.append(save_path) 314 | return save_paths 315 | 316 | def _metadata_from_raw(self, raw): 317 | """Return a DataFrame containing metadata. 318 | 319 | Add corresponding metadata for each epoch in epochs. 320 | 321 | Parameters 322 | ---------- 323 | raw : mne.Raw 324 | An instance of Raw. 325 | 326 | Returns 327 | ------- 328 | events : array, shape (n, 3) 329 | The events corresponding to the generated metadata, i.e. 330 | one time-locked event per row. 331 | event_id : dict 332 | The event dictionary corresponding to the new events array. 333 | metadata : DataFrame 334 | Metadata corresponding to epochs. 335 | 336 | Notes 337 | ----- 338 | Metadata is usually made based on the information 339 | contained in the raw file, such as the type of experimental 340 | stimulus represented by the file name, the subject responds time, 341 | the accuracy rate, etc. 342 | """ 343 | pass 344 | 345 | def _make_metadata(self, metadata): 346 | """Build and return new metadata based on existing metadata. 347 | 348 | Create high-level metadata based on metadata extracted from raw files 349 | 350 | Parameters 351 | ---------- 352 | metadata : DataFrame 353 | Metadata corresponding to epochs. 354 | 355 | Returns 356 | ------- 357 | metadata : DataFrame 358 | New metadata that contains more information. 359 | 360 | Notes 361 | ----- 362 | Metadata extracted from raw data may not be enough. 363 | Therefore, it is necessary to construct high-level metadata 364 | based on existing metadata. Such as constructing SRT scores 365 | based on the subject's response time and accuracy. 366 | """ 367 | return metadata 368 | 369 | def _filter_epochs(self, epochs): 370 | """Return the epochs you want to keep. 371 | 372 | Parameters 373 | ---------- 374 | epochs : mne.Epochs 375 | Epochs instance made in self.make_epochs() 376 | 377 | Returns 378 | ------- 379 | epochs : mne.Epochs 380 | Epochs that meet the rules. 381 | 382 | Notes 383 | ----- 384 | """ 385 | return epochs 386 | 387 | def _transform_event_id(self, raw): 388 | """Transform the description in the Raw. 389 | 390 | Transform the description from numbers or letters to the 391 | format of xxx/xxx/xxx. i.e. 'stimulus/compatible/target_left'. 392 | You can learn more details on 393 | "https://mne.tools/stable/auto_tutorials/epochs/40_autogenerate_metadata.html" 394 | 395 | Parameters 396 | ---------- 397 | epochs : mne.Epochs 398 | Epochs instance made in self.make_epochs() 399 | 400 | Returns 401 | ------- 402 | None 403 | 404 | Notes 405 | ----- 406 | """ 407 | pass 408 | --------------------------------------------------------------------------------