braindecode 0.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- Braindecode-0.5.1.dist-info/LICENSE.txt +31 -0
- Braindecode-0.5.1.dist-info/METADATA +133 -0
- Braindecode-0.5.1.dist-info/RECORD +69 -0
- Braindecode-0.5.1.dist-info/WHEEL +5 -0
- Braindecode-0.5.1.dist-info/top_level.txt +2 -0
- braindecode/__init__.py +4 -0
- braindecode/classifier.py +258 -0
- braindecode/datasets/__init__.py +7 -0
- braindecode/datasets/base.py +293 -0
- braindecode/datasets/bbci.py +679 -0
- braindecode/datasets/moabb.py +110 -0
- braindecode/datasets/sleep_physionet.py +103 -0
- braindecode/datasets/tuh.py +230 -0
- braindecode/datautil/__init__.py +11 -0
- braindecode/datautil/mne.py +136 -0
- braindecode/datautil/preprocess.py +364 -0
- braindecode/datautil/serialization.py +111 -0
- braindecode/datautil/util.py +36 -0
- braindecode/datautil/windowers.py +435 -0
- braindecode/datautil/xy.py +77 -0
- braindecode/models/__init__.py +12 -0
- braindecode/models/deep4.py +252 -0
- braindecode/models/eegnet.py +339 -0
- braindecode/models/eegresnet.py +232 -0
- braindecode/models/functions.py +43 -0
- braindecode/models/hybrid.py +103 -0
- braindecode/models/modules.py +150 -0
- braindecode/models/shallow_fbcsp.py +158 -0
- braindecode/models/sleep_stager_chambon_2018.py +113 -0
- braindecode/models/tcn.py +143 -0
- braindecode/models/util.py +66 -0
- braindecode/regressor.py +240 -0
- braindecode/samplers/__init__.py +5 -0
- braindecode/samplers/base.py +78 -0
- braindecode/samplers/ssl.py +118 -0
- braindecode/training/__init__.py +8 -0
- braindecode/training/callbacks.py +21 -0
- braindecode/training/losses.py +17 -0
- braindecode/training/scoring.py +296 -0
- braindecode/util.py +362 -0
- braindecode/version.py +1 -0
- braindecode/visualization/__init__.py +5 -0
- braindecode/visualization/gradients.py +53 -0
- test/__init__.py +0 -0
- test/acceptance_tests/__init__.py +0 -0
- test/acceptance_tests/test_cropped_decoding.py +171 -0
- test/acceptance_tests/test_eeg_classifier.py +255 -0
- test/acceptance_tests/test_model_architectures.py +54 -0
- test/acceptance_tests/test_trialwise_decoding.py +168 -0
- test/unit_tests/__init__.py +0 -0
- test/unit_tests/datasets/__init__.py +0 -0
- test/unit_tests/datasets/test_dataset.py +227 -0
- test/unit_tests/datasets/test_mne.py +145 -0
- test/unit_tests/datasets/test_sleep_physionet.py +23 -0
- test/unit_tests/datasets/test_tuh.py +97 -0
- test/unit_tests/datasets/test_xy.py +31 -0
- test/unit_tests/datautil/__init__.py +0 -0
- test/unit_tests/datautil/test_preprocess.py +270 -0
- test/unit_tests/datautil/test_serialization.py +127 -0
- test/unit_tests/datautil/test_windowers.py +519 -0
- test/unit_tests/models/__init__.py +0 -0
- test/unit_tests/models/test_models.py +136 -0
- test/unit_tests/models/test_util.py +22 -0
- test/unit_tests/test_eeg_classifier.py +73 -0
- test/unit_tests/test_eeg_regressor.py +72 -0
- test/unit_tests/test_scoring.py +282 -0
- test/unit_tests/test_util.py +30 -0
- test/unit_tests/visualization/__init__.py +0 -0
- test/unit_tests/visualization/test_gradients.py +38 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Dataset objects for some public datasets.
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
# Authors: Hubert Banville <hubert.jbanville@gmail.com>
|
|
5
|
+
# Lukas Gemein <l.gemein@gmail.com>
|
|
6
|
+
# Simon Brandt <simonbrandt@protonmail.com>
|
|
7
|
+
# David Sabbagh <dav.sabbagh@gmail.com>
|
|
8
|
+
#
|
|
9
|
+
# License: BSD (3-clause)
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import mne
|
|
13
|
+
|
|
14
|
+
from .base import BaseDataset, BaseConcatDataset, WindowsDataset
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _find_dataset_in_moabb(dataset_name):
|
|
18
|
+
# soft dependency on moabb
|
|
19
|
+
from moabb.datasets.utils import dataset_list
|
|
20
|
+
for dataset in dataset_list:
|
|
21
|
+
if dataset_name == dataset.__name__:
|
|
22
|
+
# return an instance of the found dataset class
|
|
23
|
+
return dataset()
|
|
24
|
+
raise ValueError("'dataset_name' not found in moabb datasets")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _fetch_and_unpack_moabb_data(dataset, subject_ids):
|
|
28
|
+
data = dataset.get_data(subject_ids)
|
|
29
|
+
raws, subject_ids, session_ids, run_ids = [], [], [], []
|
|
30
|
+
for subj_id, subj_data in data.items():
|
|
31
|
+
for sess_id, sess_data in subj_data.items():
|
|
32
|
+
for run_id, raw in sess_data.items():
|
|
33
|
+
# set annotation if empty
|
|
34
|
+
if len(raw.annotations) == 0:
|
|
35
|
+
annots = _annotations_from_moabb_stim_channel(raw, dataset)
|
|
36
|
+
raw.set_annotations(annots)
|
|
37
|
+
raws.append(raw)
|
|
38
|
+
subject_ids.append(subj_id)
|
|
39
|
+
session_ids.append(sess_id)
|
|
40
|
+
run_ids.append(run_id)
|
|
41
|
+
description = pd.DataFrame({
|
|
42
|
+
'subject': subject_ids,
|
|
43
|
+
'session': session_ids,
|
|
44
|
+
'run': run_ids
|
|
45
|
+
})
|
|
46
|
+
return raws, description
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _annotations_from_moabb_stim_channel(raw, dataset):
|
|
50
|
+
# find events from stim channel
|
|
51
|
+
events = mne.find_events(raw)
|
|
52
|
+
|
|
53
|
+
# get annotations from events
|
|
54
|
+
event_desc = {k: v for v, k in dataset.event_id.items()}
|
|
55
|
+
annots = mne.annotations_from_events(events, raw.info['sfreq'], event_desc)
|
|
56
|
+
|
|
57
|
+
# set trial on and offset given by moabb
|
|
58
|
+
onset, offset = dataset.interval
|
|
59
|
+
annots.onset += onset
|
|
60
|
+
annots.duration += offset - onset
|
|
61
|
+
return annots
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def fetch_data_with_moabb(dataset_name, subject_ids):
|
|
65
|
+
# ToDo: update path to where moabb downloads / looks for the data
|
|
66
|
+
"""Fetch data using moabb.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
dataset_name: str
|
|
71
|
+
the name of a dataset included in moabb
|
|
72
|
+
subject_ids: list(int) | int
|
|
73
|
+
(list of) int of subject(s) to be fetched
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
raws: mne.Raw
|
|
78
|
+
info: pandas.DataFrame
|
|
79
|
+
"""
|
|
80
|
+
dataset = _find_dataset_in_moabb(dataset_name)
|
|
81
|
+
subject_id = [subject_ids] if isinstance(subject_ids, int) else subject_ids
|
|
82
|
+
return _fetch_and_unpack_moabb_data(dataset, subject_id)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class MOABBDataset(BaseConcatDataset):
|
|
86
|
+
"""A class for moabb datasets.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
dataset_name: name of dataset included in moabb to be fetched
|
|
91
|
+
subject_ids: list(int) | int
|
|
92
|
+
(list of) int of subject(s) to be fetched
|
|
93
|
+
"""
|
|
94
|
+
def __init__(self, dataset_name, subject_ids):
|
|
95
|
+
raws, description = fetch_data_with_moabb(dataset_name, subject_ids)
|
|
96
|
+
all_base_ds = [BaseDataset(raw, row)
|
|
97
|
+
for raw, (_, row) in zip(raws, description.iterrows())]
|
|
98
|
+
super().__init__(all_base_ds)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class BNCI2014001(MOABBDataset):
|
|
102
|
+
"""See moabb.datasets.bnci.BNCI2014001"""
|
|
103
|
+
def __init__(self, *args, **kwargs):
|
|
104
|
+
super().__init__("BNCI2014001", *args, **kwargs)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class HGD(MOABBDataset):
|
|
108
|
+
"""See moabb.datasets.schirrmeister2017.Schirrmeister2017"""
|
|
109
|
+
def __init__(self, *args, **kwargs):
|
|
110
|
+
super().__init__("Schirrmeister2017", *args, **kwargs)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Authors: Hubert Banville <hubert.jbanville@gmail.com>
|
|
2
|
+
#
|
|
3
|
+
# License: BSD (3-clause)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import mne
|
|
11
|
+
from mne.datasets.sleep_physionet.age import fetch_data
|
|
12
|
+
|
|
13
|
+
from .base import BaseDataset, BaseConcatDataset
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SleepPhysionet(BaseConcatDataset):
|
|
17
|
+
"""Sleep Physionet dataset.
|
|
18
|
+
|
|
19
|
+
Sleep dataset from https://physionet.org/content/sleep-edfx/1.0.0/.
|
|
20
|
+
Contains overnight recordings from 78 healthy subjects.
|
|
21
|
+
|
|
22
|
+
See [MNE example](https://mne.tools/stable/auto_tutorials/sample-datasets/plot_sleep.html).
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
subject_ids: list(int) | int | None
|
|
27
|
+
(list of) int of subject(s) to be loaded. If None, load all available
|
|
28
|
+
subjects.
|
|
29
|
+
recording_ids: list(int) | None
|
|
30
|
+
Recordings to load per subject (each subject except 13 has two
|
|
31
|
+
recordings). Can be [1], [2] or [1, 2] (same as None).
|
|
32
|
+
preload: bool
|
|
33
|
+
If True, preload the data of the Raw objects.
|
|
34
|
+
load_eeg_only: bool
|
|
35
|
+
If True, only load the EEG channels and discard the others (EOG, EMG,
|
|
36
|
+
temperature, respiration) to avoid resampling the other signals.
|
|
37
|
+
crop_wake_mins: float
|
|
38
|
+
Number of minutes of wake time to keep before the first sleep event
|
|
39
|
+
and after the last sleep event. Used to reduce the imbalance in this
|
|
40
|
+
dataset. Default of 30 mins.
|
|
41
|
+
"""
|
|
42
|
+
def __init__(self, subject_ids=None, recording_ids=None, preload=False,
|
|
43
|
+
load_eeg_only=True, crop_wake_mins=30):
|
|
44
|
+
if subject_ids is None:
|
|
45
|
+
subject_ids = range(83)
|
|
46
|
+
if recording_ids is None:
|
|
47
|
+
recording_ids = [1, 2]
|
|
48
|
+
|
|
49
|
+
paths = fetch_data(
|
|
50
|
+
subject_ids, recording=recording_ids, on_missing='warn')
|
|
51
|
+
|
|
52
|
+
all_base_ds = list()
|
|
53
|
+
for p in paths:
|
|
54
|
+
raw, desc = self._load_raw(
|
|
55
|
+
p[0], p[1], preload=preload, load_eeg_only=load_eeg_only,
|
|
56
|
+
crop_wake_mins=crop_wake_mins)
|
|
57
|
+
base_ds = BaseDataset(raw, desc)
|
|
58
|
+
all_base_ds.append(base_ds)
|
|
59
|
+
super().__init__(all_base_ds)
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def _load_raw(raw_fname, ann_fname, preload, load_eeg_only=True,
|
|
63
|
+
crop_wake_mins=False):
|
|
64
|
+
ch_mapping = {
|
|
65
|
+
'EOG horizontal': 'eog',
|
|
66
|
+
'Resp oro-nasal': 'misc',
|
|
67
|
+
'EMG submental': 'misc',
|
|
68
|
+
'Temp rectal': 'misc',
|
|
69
|
+
'Event marker': 'misc'
|
|
70
|
+
}
|
|
71
|
+
exclude = ch_mapping.keys() if load_eeg_only else ()
|
|
72
|
+
|
|
73
|
+
raw = mne.io.read_raw_edf(raw_fname, preload=preload, exclude=exclude)
|
|
74
|
+
annots = mne.read_annotations(ann_fname)
|
|
75
|
+
raw.set_annotations(annots, emit_warning=False)
|
|
76
|
+
|
|
77
|
+
if crop_wake_mins > 0:
|
|
78
|
+
# Find first and last sleep stages
|
|
79
|
+
mask = [
|
|
80
|
+
x[-1] in ['1', '2', '3', '4', 'R'] for x in annots.description]
|
|
81
|
+
sleep_event_inds = np.where(mask)[0]
|
|
82
|
+
|
|
83
|
+
# Crop raw
|
|
84
|
+
tmin = annots[int(sleep_event_inds[0])]['onset'] - \
|
|
85
|
+
crop_wake_mins * 60
|
|
86
|
+
tmax = annots[int(sleep_event_inds[-1])]['onset'] + \
|
|
87
|
+
crop_wake_mins * 60
|
|
88
|
+
raw.crop(tmin=tmin, tmax=tmax)
|
|
89
|
+
|
|
90
|
+
# Rename EEG channels
|
|
91
|
+
ch_names = {
|
|
92
|
+
i: i.replace('EEG ', '') for i in raw.ch_names if 'EEG' in i}
|
|
93
|
+
mne.rename_channels(raw.info, ch_names)
|
|
94
|
+
|
|
95
|
+
if not load_eeg_only:
|
|
96
|
+
raw.set_channel_types(ch_mapping)
|
|
97
|
+
|
|
98
|
+
basename = os.path.basename(raw_fname)
|
|
99
|
+
subj_nb = int(basename[3:5])
|
|
100
|
+
sess_nb = int(basename[5])
|
|
101
|
+
desc = pd.Series({'subject': subj_nb, 'recording': sess_nb}, name='')
|
|
102
|
+
|
|
103
|
+
return raw, desc
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dataset classes for the Temple University Hospital (TUH) EEG Corpus and the
|
|
3
|
+
TUH Abnormal EEG Corpus.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Authors: Lukas Gemein <l.gemein@gmail.com>
|
|
7
|
+
#
|
|
8
|
+
# License: BSD (3-clause)
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import os
|
|
12
|
+
import glob
|
|
13
|
+
|
|
14
|
+
import pandas as pd
|
|
15
|
+
import mne
|
|
16
|
+
|
|
17
|
+
from .base import BaseDataset, BaseConcatDataset
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TUH(BaseConcatDataset):
|
|
21
|
+
"""Temple University Hospital (TUH) EEG Corpus
|
|
22
|
+
(www.isip.piconepress.com/projects/tuh_eeg/html/downloads.shtml#c_tueg).
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
path: str
|
|
27
|
+
Parent directory of the dataset.
|
|
28
|
+
recording_ids: list(int) | int
|
|
29
|
+
A (list of) int of recording id(s) to be read (order matters and will
|
|
30
|
+
overwrite default chronological order, e.g. if recording_ids=[1,0],
|
|
31
|
+
then the first recording returned by this class will be chronologically
|
|
32
|
+
later then the second recording. Provide recording_ids in ascending
|
|
33
|
+
order to preserve chronological order.).
|
|
34
|
+
target_name: str
|
|
35
|
+
Can be 'gender', or 'age'.
|
|
36
|
+
preload: bool
|
|
37
|
+
If True, preload the data of the Raw objects.
|
|
38
|
+
add_physician_reports: bool
|
|
39
|
+
If True, the physician reports will be read from disk and added to the
|
|
40
|
+
description.
|
|
41
|
+
"""
|
|
42
|
+
def __init__(self, path, recording_ids=None, target_name=None,
|
|
43
|
+
preload=False, add_physician_reports=False):
|
|
44
|
+
# create an index of all files and gather easily accessible info
|
|
45
|
+
# without actually touching the files
|
|
46
|
+
file_paths = glob.glob(os.path.join(path, '**/*.edf'), recursive=True)
|
|
47
|
+
descriptions = _create_chronological_description(file_paths)
|
|
48
|
+
# limit to specified recording ids before doing slow stuff
|
|
49
|
+
if recording_ids is not None:
|
|
50
|
+
descriptions = descriptions[recording_ids]
|
|
51
|
+
# create datasets gathering more info about the files touching them
|
|
52
|
+
# reading the raws and potentially preloading the data
|
|
53
|
+
base_datasets = self._create_datasets(
|
|
54
|
+
descriptions, target_name, preload, add_physician_reports)
|
|
55
|
+
super().__init__(base_datasets)
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _create_datasets(descriptions, target_name, preload,
|
|
59
|
+
add_physician_reports):
|
|
60
|
+
# this is the second loop (slow)
|
|
61
|
+
base_datasets = []
|
|
62
|
+
for file_path_i, file_path in descriptions.loc['path'].iteritems():
|
|
63
|
+
# parse age and gender information from EDF header
|
|
64
|
+
age, gender = _parse_age_and_gender_from_edf_header(file_path)
|
|
65
|
+
raw = mne.io.read_raw_edf(file_path, preload=preload)
|
|
66
|
+
# read info relevant for preprocessing from raw without loading it
|
|
67
|
+
sfreq = raw.info['sfreq']
|
|
68
|
+
n_samples = raw.n_times
|
|
69
|
+
d = {
|
|
70
|
+
'sfreq': float(sfreq),
|
|
71
|
+
'n_samples': int(n_samples),
|
|
72
|
+
'age': int(age),
|
|
73
|
+
'gender': gender,
|
|
74
|
+
}
|
|
75
|
+
if add_physician_reports:
|
|
76
|
+
physician_report = _read_physician_report(file_path)
|
|
77
|
+
d['report'] = physician_report
|
|
78
|
+
additional_description = pd.Series(d)
|
|
79
|
+
description = pd.concat(
|
|
80
|
+
[descriptions.pop(file_path_i), additional_description])
|
|
81
|
+
base_dataset = BaseDataset(raw, description,
|
|
82
|
+
target_name=target_name)
|
|
83
|
+
base_datasets.append(base_dataset)
|
|
84
|
+
return base_datasets
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _create_chronological_description(file_paths):
|
|
88
|
+
# this is the first loop (fast)
|
|
89
|
+
descriptions = []
|
|
90
|
+
for file_path in file_paths:
|
|
91
|
+
description = _parse_description_from_file_path(file_path)
|
|
92
|
+
descriptions.append(pd.Series(description))
|
|
93
|
+
descriptions = pd.concat(descriptions, axis=1)
|
|
94
|
+
# order descriptions chronologically
|
|
95
|
+
descriptions.sort_values(
|
|
96
|
+
["year", "month", "day", "subject", "session", "segment"],
|
|
97
|
+
axis=1, inplace=True)
|
|
98
|
+
# https://stackoverflow.com/questions/42284617/reset-column-index-pandas
|
|
99
|
+
descriptions = descriptions.T.reset_index(drop=True).T
|
|
100
|
+
return descriptions
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _parse_description_from_file_path(file_path):
|
|
104
|
+
# stackoverflow.com/questions/3167154/how-to-split-a-dos-path-into-its-components-in-python # noqa
|
|
105
|
+
file_path = os.path.normpath(file_path)
|
|
106
|
+
tokens = file_path.split(os.sep)
|
|
107
|
+
# expect file paths as tuh_eeg/version/file_type/reference/data_split/
|
|
108
|
+
# subject/recording session/file
|
|
109
|
+
# e.g. tuh_eeg/v1.1.0/edf/01_tcp_ar/027/00002729/
|
|
110
|
+
# s001_2006_04_12/00002729_s001.edf
|
|
111
|
+
version = tokens[-7]
|
|
112
|
+
year, month, day = tokens[-2].split('_')[1:]
|
|
113
|
+
subject_id = tokens[-3]
|
|
114
|
+
session = tokens[-2].split('_')[0]
|
|
115
|
+
segment = tokens[-1].split('_')[-1].split('.')[-2]
|
|
116
|
+
reference = tokens[-5].split('_')[2]
|
|
117
|
+
return {
|
|
118
|
+
'path': file_path,
|
|
119
|
+
'version': version,
|
|
120
|
+
'year': int(year),
|
|
121
|
+
'month': int(month),
|
|
122
|
+
'day': int(day),
|
|
123
|
+
'subject': int(subject_id),
|
|
124
|
+
'session': int(session[1:]),
|
|
125
|
+
'segment': int(segment[1:]),
|
|
126
|
+
'reference': reference,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _read_physician_report(file_path):
|
|
131
|
+
directory = os.path.dirname(file_path)
|
|
132
|
+
txt_file = glob.glob(os.path.join(directory, '**/*.txt'), recursive=True)
|
|
133
|
+
# check that there is at most one txt file in the same directory
|
|
134
|
+
assert len(txt_file) in [0, 1]
|
|
135
|
+
report = ''
|
|
136
|
+
if txt_file:
|
|
137
|
+
txt_file = txt_file[0]
|
|
138
|
+
# somewhere in the corpus, encoding apparently changed
|
|
139
|
+
# first try to read as utf-8, if it does not work use latin-1
|
|
140
|
+
try:
|
|
141
|
+
with open(txt_file, 'r', encoding='utf-8') as f:
|
|
142
|
+
report = f.read()
|
|
143
|
+
except UnicodeDecodeError:
|
|
144
|
+
with open(txt_file, 'r', encoding='latin-1') as f:
|
|
145
|
+
report = f.read()
|
|
146
|
+
return report
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _read_edf_header(file_path):
|
|
150
|
+
f = open(file_path, "rb")
|
|
151
|
+
header = f.read(88)
|
|
152
|
+
f.close()
|
|
153
|
+
return header
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _parse_age_and_gender_from_edf_header(file_path):
|
|
157
|
+
header = _read_edf_header(file_path)
|
|
158
|
+
# bytes 8 to 88 contain ascii local patient identification
|
|
159
|
+
# see https://www.teuniz.net/edfbrowser/edf%20format%20description.html
|
|
160
|
+
patient_id = header[8:].decode("ascii")
|
|
161
|
+
age = -1
|
|
162
|
+
found_age = re.findall(r"Age:(\d+)", patient_id)
|
|
163
|
+
if len(found_age) == 1:
|
|
164
|
+
age = int(found_age[0])
|
|
165
|
+
gender = "X"
|
|
166
|
+
found_gender = re.findall(r"\s([F|M])\s", patient_id)
|
|
167
|
+
if len(found_gender) == 1:
|
|
168
|
+
gender = found_gender[0]
|
|
169
|
+
return age, gender
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TUHAbnormal(TUH):
|
|
173
|
+
"""Temple University Hospital (TUH) Abnormal EEG Corpus.
|
|
174
|
+
see www.isip.piconepress.com/projects/tuh_eeg/html/downloads.shtml#c_tuab
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
path: str
|
|
179
|
+
Parent directory of the dataset.
|
|
180
|
+
recording_ids: list(int) | int
|
|
181
|
+
A (list of) int of recording id(s) to be read (order matters and will
|
|
182
|
+
overwrite default chronological order, e.g. if recording_ids=[1,0],
|
|
183
|
+
then the first recording returned by this class will be chronologically
|
|
184
|
+
later then the second recording. Provide recording_ids in ascending
|
|
185
|
+
order to preserve chronological order.).
|
|
186
|
+
target_name: str
|
|
187
|
+
Can be 'pathological', 'gender', or 'age'.
|
|
188
|
+
preload: bool
|
|
189
|
+
If True, preload the data of the Raw objects.
|
|
190
|
+
add_physician_reports: bool
|
|
191
|
+
If True, the physician reports will be read from disk and added to the
|
|
192
|
+
description.
|
|
193
|
+
"""
|
|
194
|
+
def __init__(self, path, recording_ids=None, target_name='pathological',
|
|
195
|
+
preload=False, add_physician_reports=False):
|
|
196
|
+
super().__init__(path=path, recording_ids=recording_ids,
|
|
197
|
+
preload=preload,
|
|
198
|
+
add_physician_reports=add_physician_reports)
|
|
199
|
+
additional_descriptions = []
|
|
200
|
+
for file_path in self.description.path:
|
|
201
|
+
additional_description = (
|
|
202
|
+
self._parse_additional_description_from_file_path(file_path))
|
|
203
|
+
additional_descriptions.append(additional_description)
|
|
204
|
+
additional_descriptions = pd.DataFrame(additional_descriptions)
|
|
205
|
+
self.description = pd.concat(
|
|
206
|
+
[self.description, additional_descriptions], axis=1)
|
|
207
|
+
# not 100% sure if this is required:
|
|
208
|
+
# set target name and target of base datasets
|
|
209
|
+
for ds_i, ds in enumerate(self.datasets):
|
|
210
|
+
ds.target_name = target_name
|
|
211
|
+
ds.target = self.description.loc[ds_i, target_name]
|
|
212
|
+
for k in additional_descriptions.columns:
|
|
213
|
+
ds.description[k] = self.description.loc[ds_i, k]
|
|
214
|
+
|
|
215
|
+
@staticmethod
|
|
216
|
+
def _parse_additional_description_from_file_path(file_path):
|
|
217
|
+
file_path = os.path.normpath(file_path)
|
|
218
|
+
tokens = file_path.split(os.sep)
|
|
219
|
+
# expect paths as version/file type/data_split/pathology status/
|
|
220
|
+
# reference/subset/subject/recording session/file
|
|
221
|
+
# e.g. v2.0.0/edf/train/normal/01_tcp_ar/000/00000021/
|
|
222
|
+
# s004_2013_08_15/00000021_s004_t000.edf
|
|
223
|
+
assert ('abnormal' in tokens or 'normal' in tokens), (
|
|
224
|
+
'No pathology labels found.')
|
|
225
|
+
assert ('train' in tokens or 'eval' in tokens), (
|
|
226
|
+
'No train or eval set information found.')
|
|
227
|
+
return {
|
|
228
|
+
'train': 'train' in tokens,
|
|
229
|
+
'pathological': 'abnormal' in tokens,
|
|
230
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for data manipulation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .windowers import create_windows_from_events, create_fixed_length_windows
|
|
6
|
+
from .preprocess import (zscore, scale, exponential_moving_demean,
|
|
7
|
+
exponential_moving_standardize, filterbank,
|
|
8
|
+
preprocess, Preprocessor)
|
|
9
|
+
from .xy import create_from_X_y
|
|
10
|
+
from .mne import create_from_mne_raw, create_from_mne_epochs
|
|
11
|
+
from .serialization import save_concat_dataset, load_concat_dataset
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import mne
|
|
4
|
+
|
|
5
|
+
from ..datasets.base import BaseDataset, BaseConcatDataset, WindowsDataset
|
|
6
|
+
from ..datautil.windowers import (
|
|
7
|
+
_check_windowing_arguments, create_windows_from_events)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_from_mne_raw(
|
|
11
|
+
raws, trial_start_offset_samples, trial_stop_offset_samples,
|
|
12
|
+
window_size_samples, window_stride_samples, drop_last_window,
|
|
13
|
+
descriptions=None, mapping=None, preload=False, drop_bad_windows=True):
|
|
14
|
+
"""Create WindowsDatasets from mne.RawArrays
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
raws: array-like
|
|
19
|
+
list of mne.RawArrays
|
|
20
|
+
trial_start_offset_samples: int
|
|
21
|
+
start offset from original trial onsets in samples
|
|
22
|
+
trial_stop_offset_samples: int
|
|
23
|
+
stop offset from original trial stop in samples
|
|
24
|
+
window_size_samples: int
|
|
25
|
+
window size
|
|
26
|
+
window_stride_samples: int
|
|
27
|
+
stride between windows
|
|
28
|
+
drop_last_window: bool
|
|
29
|
+
whether or not have a last overlapping window, when
|
|
30
|
+
windows do not equally divide the continuous signal
|
|
31
|
+
descriptions: array-like
|
|
32
|
+
list of dicts or pandas.Series with additional information about the raws
|
|
33
|
+
mapping: dict(str: int)
|
|
34
|
+
mapping from event description to target value
|
|
35
|
+
preload: bool
|
|
36
|
+
if True, preload the data of the Epochs objects.
|
|
37
|
+
drop_bad_windows: bool
|
|
38
|
+
If True, call `.drop_bad()` on the resulting mne.Epochs object. This
|
|
39
|
+
step allows identifying e.g., windows that fall outside of the
|
|
40
|
+
continuous recording. It is suggested to run this step here as otherwise
|
|
41
|
+
the BaseConcatDataset has to be updated as well.
|
|
42
|
+
|
|
43
|
+
Returns
|
|
44
|
+
-------
|
|
45
|
+
windows_datasets: BaseConcatDataset
|
|
46
|
+
X and y transformed to a dataset format that is compativle with skorch
|
|
47
|
+
and braindecode
|
|
48
|
+
"""
|
|
49
|
+
if descriptions is not None:
|
|
50
|
+
if len(descriptions) != len(raws):
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"length of 'raws' ({len(raws)}) and 'description' "
|
|
53
|
+
f"({len(descriptions)}) has to match")
|
|
54
|
+
base_datasets = [BaseDataset(raw, desc) for raw, desc in
|
|
55
|
+
zip(raws, descriptions)]
|
|
56
|
+
else:
|
|
57
|
+
base_datasets = [BaseDataset(raw) for raw in raws]
|
|
58
|
+
|
|
59
|
+
base_datasets = BaseConcatDataset(base_datasets)
|
|
60
|
+
windows_datasets = create_windows_from_events(
|
|
61
|
+
base_datasets,
|
|
62
|
+
trial_start_offset_samples=trial_start_offset_samples,
|
|
63
|
+
trial_stop_offset_samples=trial_stop_offset_samples,
|
|
64
|
+
window_size_samples=window_size_samples,
|
|
65
|
+
window_stride_samples=window_stride_samples,
|
|
66
|
+
drop_last_window=drop_last_window,
|
|
67
|
+
mapping=mapping,
|
|
68
|
+
drop_bad_windows=drop_bad_windows,
|
|
69
|
+
preload=preload
|
|
70
|
+
)
|
|
71
|
+
return windows_datasets
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def create_from_mne_epochs(list_of_epochs, window_size_samples,
|
|
75
|
+
window_stride_samples, drop_last_window):
|
|
76
|
+
"""Create WindowsDatasets from mne.Epochs
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
list_of_epochs: array-like
|
|
81
|
+
list of mne.Epochs
|
|
82
|
+
window_size_samples: int
|
|
83
|
+
window size
|
|
84
|
+
window_stride_samples: int
|
|
85
|
+
stride between windows
|
|
86
|
+
drop_last_window: bool
|
|
87
|
+
whether or not have a last overlapping window, when
|
|
88
|
+
windows do not equally divide the continuous signal
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
windows_datasets: BaseConcatDataset
|
|
93
|
+
X and y transformed to a dataset format that is compativle with skorch
|
|
94
|
+
and braindecode
|
|
95
|
+
"""
|
|
96
|
+
_check_windowing_arguments(0, 0, window_size_samples,
|
|
97
|
+
window_stride_samples)
|
|
98
|
+
|
|
99
|
+
list_of_windows_ds = []
|
|
100
|
+
for epochs in list_of_epochs:
|
|
101
|
+
event_descriptions = epochs.events[:, 2]
|
|
102
|
+
original_trial_starts = epochs.events[:, 0]
|
|
103
|
+
stop = len(epochs.times) - window_size_samples
|
|
104
|
+
|
|
105
|
+
# already includes last incomplete window start
|
|
106
|
+
starts = np.arange(0, stop + 1, window_stride_samples)
|
|
107
|
+
|
|
108
|
+
if not drop_last_window and starts[-1] < stop:
|
|
109
|
+
# if last window does not end at trial stop, make it stop there
|
|
110
|
+
starts = np.append(starts, stop)
|
|
111
|
+
|
|
112
|
+
fake_events = [[start, window_size_samples, -1] for start in
|
|
113
|
+
starts]
|
|
114
|
+
|
|
115
|
+
for trial_i, trial in enumerate(epochs):
|
|
116
|
+
metadata = pd.DataFrame({
|
|
117
|
+
'i_window_in_trial': np.arange(len(fake_events)),
|
|
118
|
+
'i_start_in_trial': starts + original_trial_starts[trial_i],
|
|
119
|
+
'i_stop_in_trial': starts + original_trial_starts[
|
|
120
|
+
trial_i] + window_size_samples,
|
|
121
|
+
'target': len(fake_events) * [event_descriptions[trial_i]]
|
|
122
|
+
})
|
|
123
|
+
# window size - 1, since tmax is inclusive
|
|
124
|
+
mne_epochs = mne.Epochs(
|
|
125
|
+
mne.io.RawArray(trial, epochs.info), fake_events,
|
|
126
|
+
baseline=None,
|
|
127
|
+
tmin=0,
|
|
128
|
+
tmax=(window_size_samples - 1) / epochs.info["sfreq"],
|
|
129
|
+
metadata=metadata)
|
|
130
|
+
|
|
131
|
+
mne_epochs.drop_bad(reject=None, flat=None)
|
|
132
|
+
|
|
133
|
+
windows_ds = WindowsDataset(mne_epochs)
|
|
134
|
+
list_of_windows_ds.append(windows_ds)
|
|
135
|
+
|
|
136
|
+
return BaseConcatDataset(list_of_windows_ds)
|