MorganBrizon commited on
Commit
0f7fdb9
·
verified ·
1 Parent(s): f0710b4

Upload 4 files

Browse files
Files changed (4) hide show
  1. app.py +69 -0
  2. prediction.py +261 -0
  3. preprocessing.py +101 -0
  4. preprocessing_epilepsynet.py +260 -0
app.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ import uvicorn
4
+ from fastapi import FastAPI, UploadFile, File, HTTPException
5
+ from fastapi.responses import JSONResponse
6
+
7
+ # Import your prediction functions
8
+ from prediction import predict_eeg_recording, predict_ensemble_eeg_recording
9
+
10
+ app = FastAPI(title="EEG Epilepsy Prediction API")
11
+
12
+ @app.get("/", tags=["Introduction Endpoints"])
13
+ async def index():
14
+ """
15
+ Simply returns a welcome message!
16
+ """
17
+ message = (
18
+ "Hello world! Welcome to the EEG Epilepsy Prediction API. "
19
+ "Submit an EEG recording EDF file to the `/predict` endpoint to receive a prediction."
20
+ )
21
+ return message
22
+
23
+ @app.post("/predict", tags=["Machine Learning"])
24
+ async def predict_endpoint(
25
+ file: UploadFile = File(...),
26
+ model_choice: str = "2DCNN",
27
+ ensemble_method: str = None
28
+ ):
29
+ """
30
+
31
+ Query parameters:
32
+ - model_choice: Choose one model among "2DCNN", "EEGNet", "EpilepsyNet", or "ensemble".
33
+ - ensemble_method: (Optional, required if model_choice is "ensemble")
34
+ The ensemble method to use ("average" or "voting").
35
+
36
+ """
37
+ print("Saving uploaded file as temporary file...")
38
+ try:
39
+ suffix = os.path.splitext(file.filename)[1]
40
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
41
+ tmp.write(await file.read())
42
+ tmp_path = tmp.name
43
+ except Exception as e:
44
+ raise HTTPException(status_code=500, detail="Error saving temporary file")
45
+
46
+ print("Performing prediction using model_choice =", model_choice)
47
+ try:
48
+ if model_choice.lower() == "ensemble":
49
+ if ensemble_method is None:
50
+ raise HTTPException(status_code=400, detail="ensemble_method must be specified when using ensemble model_choice")
51
+ pred_label, mean_prob = predict_ensemble_eeg_recording(tmp_path, ensemble_method=ensemble_method, threshold=0.5)
52
+ else:
53
+ pred_label, mean_prob = predict_eeg_recording(tmp_path, model_name=model_choice, threshold=0.5)
54
+ except Exception as e:
55
+ os.remove(tmp_path)
56
+ raise HTTPException(status_code=400, detail=f"Prediction failed: {e}")
57
+
58
+ os.remove(tmp_path)
59
+
60
+
61
+ response = {
62
+ "prediction": "epilepsy" if pred_label == 1 else "no epilepsy",
63
+ "confidence": mean_prob
64
+ }
65
+ print("Prediction complete, returning response...")
66
+ return JSONResponse(content=response)
67
+
68
+ if __name__ == "__main__":
69
+ uvicorn.run(app, host="0.0.0.0", port=7860)
prediction.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+ import tensorflow as tf
4
+ import pandas as pd
5
+ import joblib
6
+ from sklearn.metrics import confusion_matrix, f1_score, accuracy_score
7
+ from preprocessing import preprocess_eeg_file
8
+ from preprocessing_2dcnn import convert_epoch_to_spectrogram
9
+ from preprocessing_epilepsynet import *
10
+ from EpilepsyNet_model import TimeSeriesAttentionClassifier
11
+ from eegnet_model import EEGNet
12
+
13
+
14
+
15
+ def aggregate_predictions(spectrogram_list, model, threshold=0.5):
16
+
17
+ # Convert each spectrogram to channels-last format.
18
+ X = np.array([np.transpose(s, (1, 2, 0)) for s in spectrogram_list])
19
+ print(f'---Aggregating predictions from {len(spectrogram_list)} segments---')
20
+ preds = model.predict(X)
21
+ mean_prob = np.mean(preds[:, 1])
22
+ final_label = 1 if mean_prob >= threshold else 0
23
+ return final_label, mean_prob
24
+
25
+ def predict_eeg_recording(edf_path, model_name='2DCNN', threshold=0.5):
26
+
27
+
28
+ if model_name == '2DCNN':
29
+
30
+ model = tf.keras.models.load_model('model1_2dcnn.h5')
31
+ channels = ["EEG FP1-REF", "EEG FP2-REF", "EEG F3-REF", "EEG F4-REF", "EEG C3-REF"]
32
+ #Process the edf file
33
+ preprocessed_df = preprocess_eeg_file(edf_path, fmin=1.0, fmax=45.0, segment_lenght=5, overlap=2,desired=channels)
34
+
35
+ if preprocessed_df is None or preprocessed_df.empty:
36
+ raise ValueError("EEG file could not be preprocessed or no valid segments found.")
37
+
38
+ # Convert each 5-second segment (each row) into a spectrogram.
39
+ spectrogram_list = preprocessed_df.apply(
40
+ lambda row: convert_epoch_to_spectrogram(row, channels, fs=250, nperseg=128, noverlap=64), axis=1
41
+ ).tolist()
42
+
43
+ return aggregate_predictions(spectrogram_list, model, threshold)
44
+
45
+ elif model_name == 'EEGNet':
46
+ loaded = joblib.load("eegnet_model.joblib")
47
+ # Extract the actual state dictionary.
48
+ state_dict = loaded["model_state_dict"]
49
+
50
+ model = EEGNet(n_channels=21, n_samples=1250, num_classes=2)
51
+ model.load_state_dict(state_dict)
52
+
53
+ # Define the channels to use for EEGNet
54
+ channels = [
55
+ 'EEG FP1-REF', # Left frontal pole
56
+ 'EEG FP2-REF', # Right frontal pole
57
+ 'EEG F3-REF', # Left frontal
58
+ 'EEG F4-REF', # Right frontal
59
+ 'EEG C3-REF', # Left central
60
+ 'EEG C4-REF', # Right central
61
+ 'EEG P3-REF', # Left parietal
62
+ 'EEG P4-REF', # Right parietal
63
+ 'EEG O1-REF', # Left occipital
64
+ 'EEG O2-REF', # Right occipital
65
+ 'EEG F7-REF', # Left lateral frontal
66
+ 'EEG F8-REF', # Right lateral frontal
67
+ 'EEG T3-REF', # Left temporal (anterior)
68
+ 'EEG T4-REF', # Right temporal (anterior)
69
+ 'EEG T5-REF', # Left temporal (posterior)
70
+ 'EEG T6-REF', # Right temporal (posterior)
71
+ 'EEG FZ-REF', # Frontal midline
72
+ 'EEG CZ-REF', # Central midline
73
+ 'EEG PZ-REF', # Parietal midline
74
+ 'EEG ROC-REF', # Right occipital (often used as reference or an extra site)
75
+ 'EEG LOC-REF' # Left occipital (often used as reference or an extra site)
76
+ ]
77
+ # Process the edf file
78
+ preprocessed_df = preprocess_eeg_file(
79
+ edf_path, fmin=1.0, fmax=45.0, segment_lenght=5, overlap=0, desired=channels
80
+ )
81
+
82
+ if preprocessed_df is None or preprocessed_df.empty:
83
+ raise ValueError("EEG file could not be preprocessed or no valid segments found.")
84
+
85
+ # For EEGNet, we use the raw time series data directly.
86
+ # Convert each 5-second segment (row) to a 2D timeseries array of shape (n_channels, n_samples)
87
+ timeseries_list = preprocessed_df.apply(
88
+ lambda row: convert_epoch_to_timeseries(row, channels), axis=1
89
+ ).tolist()
90
+
91
+ return aggregate_predictions_EEGNET(timeseries_list, model, threshold)
92
+
93
+ elif model_name == 'EpilepsyNet':
94
+
95
+ raw = mne.io.read_raw_edf(edf_path,
96
+ preload=True,
97
+ verbose='ERROR')
98
+
99
+ eeg_cols = ['EEG FP1', 'EEG FP2', 'EEG F3', 'EEG F4',
100
+ 'EEG C3', 'EEG C4', 'EEG P3', 'EEG P4',
101
+ 'EEG O1', 'EEG O2', 'EEG F7', 'EEG F8',
102
+ 'EEG T3', 'EEG T4', 'EEG T5', 'EEG T6',
103
+ 'EEG T1', 'EEG T2', 'EEG FZ', 'EEG CZ',
104
+ 'EEG PZ']
105
+
106
+ parameters = {
107
+ 'eeg_cols':eeg_cols,
108
+ 'segment_duration':60.0, # 60 second segments
109
+ 'n_segments_per_file':12, # Split into 12 epochs (5 sec each)
110
+ 'samples_per_segment':1250, # 1250 samples per segment (250 Hz sampling rate)
111
+ 'random_state':42
112
+ }
113
+
114
+ X = process_raw_files(
115
+ raw_file=raw,
116
+ eeg_cols=eeg_cols,
117
+ segment_duration=parameters['segment_duration'],
118
+ n_segments_per_file=parameters['n_segments_per_file'],
119
+ random_state=parameters['random_state']
120
+ )
121
+
122
+ X_std = standardize_data(X)
123
+ corr_matrix = compute_correlation_matrix(X_std)
124
+ # print('Correlation matrix shape :',corr_matrix.shape)
125
+
126
+ upper_triangle_matrix = extract_upper_triangle(corr_matrix)
127
+ # print('Upper Triangle shape :',upper_triangle_matrix.shape)
128
+
129
+ X_tensor = torch.tensor(upper_triangle_matrix, dtype=torch.float32)
130
+ X_tensor = X_tensor.unsqueeze(0)
131
+
132
+ # Model parameters
133
+ input_dim = 210 # Size of flattened upper triangle (21*20/2)
134
+ embed_dim = 256 # Embedding dimension
135
+ num_heads = 16 # Number of attention heads7
136
+
137
+ model = TimeSeriesAttentionClassifier(input_dim, embed_dim, num_heads)
138
+ model.load_state_dict(torch.load('EpilepsyNet.pth'))
139
+ model.eval()
140
+ print('¨'*50)
141
+ print('Model Prediction :')
142
+
143
+ outputs, _ = model(X_tensor)
144
+ # For binary classification with sigmoid, prediction is 1 if output > 0.5
145
+ predicted = (outputs >= 0.5).float()
146
+
147
+ return int(predicted), outputs.float().squeeze().item()
148
+
149
+
150
+ def convert_epoch_to_timeseries(epoch_row, channels):
151
+
152
+ ts_list = []
153
+ for ch in channels:
154
+ # Check if the channel is in the epoch_row; if not, skip it.
155
+ if ch in epoch_row:
156
+ ts = epoch_row[ch]
157
+ ts_list.append(ts)
158
+ return np.stack(ts_list, axis=0)
159
+
160
+
161
+ def aggregate_predictions_EEGNET(segment_list, model, threshold):
162
+ """
163
+ Given a list of segments (raw time series data for EEGNet) and a trained
164
+ PyTorch model, predict on each segment and then aggregate the predictions.
165
+
166
+ For each segment, the model returns a probability vector.
167
+ This function averages the predicted probabilities across segments,
168
+ and then compares the average probability for class 1 with the provided threshold
169
+ to decide the final predicted class.
170
+
171
+ Parameters:
172
+ segment_list : list of numpy arrays
173
+ Each element is a 2D numpy array with shape (n_channels, n_samples)
174
+ representing one EEG segment.
175
+ model : a trained PyTorch model that accepts input of shape
176
+ (batch_size, n_channels, n_samples) and outputs probabilities (or logits) for each class.
177
+ threshold : float
178
+ The probability threshold to decide class 1.
179
+
180
+ Returns:
181
+ final_class : int
182
+ The aggregated predicted class (0 or 1).
183
+ """
184
+ model.eval()
185
+ preds = []
186
+
187
+ with torch.no_grad():
188
+ for seg in segment_list:
189
+ # Convert the segment to a torch tensor (float32)
190
+ # Expected shape: (n_channels, n_samples)
191
+ seg_tensor = torch.tensor(seg, dtype=torch.float32)
192
+ # Add a batch dimension -> shape: (1, 1, n_channels, n_samples)
193
+ seg_tensor = seg_tensor.unsqueeze(0).unsqueeze(0)
194
+
195
+ # Forward pass: get the model's output.
196
+ # If your model returns logits, you may need to apply softmax.
197
+ output = model(seg_tensor)
198
+
199
+ # Check if the output is probabilities already or logits.
200
+ # For safety, let's apply softmax to ensure we have probabilities.
201
+ prob = torch.softmax(output, dim=1)[0].cpu().numpy()
202
+
203
+ preds.append(prob)
204
+
205
+ # Average the predictions over all segments
206
+ avg_pred = np.mean(preds, axis=0)
207
+ # For binary classification assume avg_pred[1] is the probability for class 1.
208
+ final_class = int(avg_pred[1] >= threshold)
209
+
210
+ return final_class, avg_pred[1]
211
+
212
+
213
+
214
+ import numpy as np
215
+
216
+ def predict_ensemble_eeg_recording(edf_path, ensemble_method, threshold=0.5):
217
+ """
218
+ The function aggregates the probability scalars from each model and then:
219
+ - For soft voting (method="average"): averages the probabilities
220
+ - For hard voting (method="voting"): uses majority voting (each model votes 1 if
221
+ its probability is >= threshold, else 0)
222
+
223
+ Parameters:
224
+ edf_path : str
225
+ Path to the EEG EDF file.
226
+ ensemble_method : str
227
+ Aggregation method, either "average" for soft voting or "voting" for hard voting.
228
+ threshold : float, default=0.5
229
+ The probability threshold to decide class 1.
230
+
231
+ Returns:
232
+ final_class : int
233
+ The final aggregated predicted class (0 or 1).
234
+ aggregated : float or list
235
+ For "average", the average probability as a float;
236
+ for "voting", the list of votes from each model.
237
+ """
238
+ # Lists to collect probabilities and votes.
239
+ pred_prob_list = []
240
+ votes = []
241
+
242
+ # Iterate over the three model types.
243
+ for model_name in ["2DCNN", "EEGNet", "EpilepsyNet"]:
244
+ pred_label, prob = predict_eeg_recording(edf_path, model_name=model_name, threshold=threshold)
245
+ pred_prob_list.append(prob)
246
+ votes.append(int(prob >= threshold))
247
+ print(f"Prediction from {model_name}: label={pred_label}, probability={prob}")
248
+
249
+ if ensemble_method.lower() == "average":
250
+ # Soft voting: average the probabilities.
251
+ avg_prob = np.mean(pred_prob_list)
252
+ final_class = int(avg_prob >= threshold)
253
+ print("Averaged probability:", avg_prob)
254
+ return final_class, avg_prob
255
+ elif ensemble_method.lower() == "voting":
256
+ # Hard voting: majority decision.
257
+ final_class = int(round(np.mean(votes)))
258
+ print("Votes from each model:", votes)
259
+ return final_class, votes
260
+ else:
261
+ raise ValueError("Ensemble method must be either 'average' or 'voting'.")
preprocessing.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import mne
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+
7
+ def standardize_dataframe(df):
8
+ # Make a copy to avoid modifying the original dataframe
9
+ df_standardized = df.copy()
10
+
11
+ # Only standardize numeric columns
12
+ numeric_columns = df.select_dtypes(include=np.number).columns
13
+
14
+ for column in numeric_columns:
15
+ mean = df[column].mean()
16
+ std = df[column].std()
17
+
18
+ df_standardized[column] = (df[column] - mean) / std
19
+
20
+ return df_standardized
21
+
22
+ desired = ["EEG FP1-REF", "EEG FP2-REF",
23
+ "EEG F3-REF", "EEG F4-REF",
24
+ "EEG C3-REF"]
25
+
26
+ def select_relevant_channels(raw, desired = desired):
27
+
28
+ # For relevant channel criteria check documentation
29
+ '''“EEG FP1-REF” for the left frontal pole
30
+
31
+ “EEG FP2-REF” for the right frontal pole
32
+
33
+ “EEG F3-REF” for the left frontal region
34
+
35
+ “EEG F4-REF” for the right frontal region
36
+
37
+ “EEG C3-REF” for the left central region'''
38
+
39
+
40
+ #check if all desired channels are present; if not, skip this file
41
+ if not all(ch in raw.ch_names for ch in desired):
42
+ print("Skipping file because it doesn't have the full set of desired channels.")
43
+ return None
44
+ raw.pick_channels(desired, verbose=False)
45
+ return raw
46
+
47
+
48
+ def collapse_epoch_df_by_channel(epoch_df):
49
+ # Identify channel columns (exclude time, epoch, condition)
50
+ channel_cols = [col for col in epoch_df.columns if col not in ['time', 'epoch']]
51
+ # Group by epoch
52
+ grouped = epoch_df.groupby('epoch')
53
+ rows = []
54
+ for epoch_num, group in grouped:
55
+ group_sorted = group.sort_values('time')
56
+ # For each channel, extract the 1D array for this epoch
57
+ row = {'epoch': epoch_num}
58
+ for ch in channel_cols:
59
+ row[ch] = group_sorted[ch].values # 1D array of length = number of time samples in the epoch
60
+ rows.append(row)
61
+ return pd.DataFrame(rows)
62
+
63
+ def preprocess_eeg_file(edf_path, fmin=1.0, fmax=45.0, segment_lenght=5, overlap=2,desired=desired):
64
+
65
+ # 1. Charger le fichier EDF avec MNE
66
+ raw = mne.io.read_raw_edf(edf_path, preload=True, verbose=False)
67
+
68
+ # Resample (to 250 because it's the lowest sampling rate )
69
+ raw.resample(250, verbose=False)
70
+
71
+ # Filtrage passe-bande (1-45 Hz)
72
+ raw.filter(fmin, fmax, fir_design='firwin', verbose=False)
73
+
74
+ # Skip EEGs less than 5s
75
+ if raw.times[-1] < 5:
76
+ print(f"Skipping {edf_path}: duration ({raw.times[-1]:.2f} s) is less than required 5s.")
77
+ return None
78
+
79
+ # Suppression des canaux non EEG
80
+ eeg_channels = mne.pick_types(raw.info, eeg=True, exclude=[])
81
+ raw.pick(eeg_channels, verbose=False)
82
+
83
+ # Selectionner les channels pertinents (channel selection from EDA ?)
84
+ print(raw.ch_names)
85
+ raw = select_relevant_channels(raw,desired=desired)
86
+ if raw is None:
87
+ return None
88
+
89
+ # Segmentation
90
+ epochs = mne.make_fixed_length_epochs(raw, duration=segment_lenght, preload=False, overlap=overlap, verbose=False)
91
+
92
+ # Transform to dataframe and standadize
93
+
94
+ df = epochs.to_data_frame() # epochs is returned by preprocess_eeg_file()
95
+ df_std = standardize_dataframe(df.drop(['time','epoch', 'condition'], axis=1))
96
+ result = pd.concat([df[['time','epoch']], df_std], axis=1)
97
+
98
+ return collapse_epoch_df_by_channel(result)
99
+
100
+
101
+
preprocessing_epilepsynet.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mne
2
+ import numpy as np
3
+
4
+ from typing import List, Optional
5
+ import random
6
+
7
+
8
+ def extract_random_segment(raw: mne.io.Raw, duration: float = 60.0,
9
+ random_state: Optional[int] = None) -> mne.io.Raw:
10
+ """
11
+ Extract a random segment of specified duration from a raw MNE file.
12
+
13
+ Parameters:
14
+ -----------
15
+ raw : mne.io.Raw
16
+ The raw MNE object
17
+ duration : float
18
+ Duration of the segment to extract in seconds
19
+ random_state : int, optional
20
+ Random seed for reproducibility
21
+
22
+ Returns:
23
+ --------
24
+ mne.io.Raw
25
+ A cropped raw object containing only the random segment
26
+ """
27
+ if random_state is not None:
28
+ np.random.seed(random_state)
29
+
30
+ # Get the total duration of the raw file
31
+ total_duration = raw.times[-1]
32
+
33
+ # Ensure the raw file is long enough
34
+ if total_duration <= duration:
35
+ raise ValueError(f"Raw file duration ({total_duration:.2f}s) is shorter than requested segment duration ({duration:.2f}s)")
36
+
37
+ # Generate a random start time
38
+ max_start = total_duration - duration
39
+ start_time = np.random.uniform(0, max_start)
40
+ end_time = start_time + duration
41
+
42
+ # Create a copy and crop to the random segment
43
+ raw_segment = raw.copy().crop(tmin=start_time, tmax=end_time)
44
+
45
+ return raw_segment
46
+
47
+
48
+ def segment_to_epochs(raw_segment: mne.io.Raw, n_segments: int = 12) -> mne.Epochs:
49
+ """
50
+ Convert a raw segment into fixed-length epochs.
51
+
52
+ Parameters:
53
+ -----------
54
+ raw_segment : mne.io.Raw
55
+ The raw segment to convert to epochs
56
+ n_segments : int
57
+ Number of segments to create
58
+
59
+ Returns:
60
+ --------
61
+ mne.Epochs
62
+ Epoch object containing the segmented data
63
+ """
64
+ # Calculate duration of each epoch based on total duration and number of segments
65
+ total_duration = raw_segment.times[-1]
66
+ epoch_duration = total_duration / n_segments
67
+
68
+ # Create fixed-length epochs
69
+ epochs = mne.make_fixed_length_epochs(
70
+ raw_segment,
71
+ duration=epoch_duration,
72
+ preload=True,
73
+ reject_by_annotation=True
74
+ )
75
+
76
+ return epochs
77
+
78
+
79
+ def process_raw_files(raw_file: mne.io.Raw,
80
+ eeg_cols: List[str],
81
+ segment_duration: float = 60.0,
82
+ n_segments_per_file: int = 12,
83
+ samples_per_segment: int = 1250,
84
+ random_state: Optional[int] = None) -> np.ndarray:
85
+ """
86
+ Process a list of raw MNE files into a batch of epochs with specific EEG channels.
87
+
88
+ Parameters:
89
+ -----------
90
+ raw_files : mne.io.Raw
91
+ The raw MNE object to make preds on
92
+ eeg_cols : List[str]
93
+ List of EEG channel names to keep
94
+ segment_duration : float
95
+ Duration of random segment to extract from each file in seconds
96
+ n_segments_per_file : int
97
+ Number of segments to create per file
98
+ samples_per_segment : int
99
+ Number of time samples per segment
100
+ random_state : int, optional
101
+ Random seed for reproducibility
102
+
103
+ Returns:
104
+ --------
105
+ np.ndarray
106
+ Array of shape (len(raw_files), n_segments_per_file, len(eeg_cols), samples_per_segment)
107
+ """
108
+ # Initialize the output array
109
+ X = np.zeros((n_segments_per_file, len(eeg_cols), samples_per_segment))
110
+
111
+ # Define duration of each epoch based on number of segment and total duration
112
+ epoch_duration = segment_duration / n_segments_per_file
113
+
114
+
115
+ try:
116
+ # Set different random seed for each file if random_state is provided
117
+ file_random_state = None if random_state is None else random_state
118
+
119
+ # Pick only the specified EEG channels
120
+ available_channels = raw_file.ch_names
121
+ print('Num of availbable ch :', len(available_channels))
122
+ channels_to_use = [ch for ch in available_channels if ch.replace('-REF','').replace('-LE','') in eeg_cols]
123
+ if not channels_to_use:
124
+ raise ValueError(f"None of the specified EEG channels found in file")
125
+
126
+ if len(channels_to_use) < len(eeg_cols):
127
+ print(f"Warning: Only {len(channels_to_use)}/{len(eeg_cols)} EEG channels found in file")
128
+
129
+ # Select only the required channels
130
+ raw_eeg = raw_file.copy().pick_channels(channels_to_use)
131
+
132
+ # Resample to 250Hz
133
+ current_sfreq = int(raw_eeg.info['sfreq'])
134
+ if current_sfreq != 250:
135
+ print(f"🔁 Resample : {current_sfreq} Hz → {250} Hz")
136
+ raw_eeg.resample(250)
137
+
138
+ # Extract random segment
139
+ raw_segment = extract_random_segment(
140
+ raw_eeg,
141
+ duration=segment_duration,
142
+ random_state=file_random_state
143
+ )
144
+
145
+ # Convert to epochs
146
+ epochs = segment_to_epochs(raw_segment, n_segments=n_segments_per_file)
147
+
148
+ # Get the data as array
149
+ epoch_data = epochs.get_data()
150
+
151
+ # Ensure the data has the correct number of time samples
152
+ if epoch_data.shape[2] != samples_per_segment:
153
+ # Resample if necessary
154
+ resampling_freq = samples_per_segment / (epoch_duration / n_segments_per_file)
155
+ raw_segment.resample(resampling_freq)
156
+ epochs = segment_to_epochs(raw_segment, n_segments=n_segments_per_file)
157
+ epoch_data = epochs.get_data()
158
+
159
+ # Store in the output array
160
+ X[:, :len(channels_to_use), :] = epoch_data
161
+
162
+ except Exception as e:
163
+ print(f"Error processing file {str(e)}")
164
+ # Keep zeros in the output array for this file
165
+
166
+ return X
167
+
168
+ # Standardize the data per channel :
169
+ def standardize_data(X: np.ndarray) -> np.ndarray:
170
+ """
171
+ Standardize the data along the last axis (time samples).
172
+
173
+ Parameters:
174
+ -----------
175
+ X : np.ndarray
176
+ Input data of shape (n_samples, n_segments, n_channels, n_time_samples)
177
+
178
+ Returns:
179
+ --------
180
+ np.ndarray
181
+ Standardized data
182
+ """
183
+ # Compute mean and std for each channel across all segments and samples
184
+ mean = np.mean(X, axis=(1), keepdims=True)
185
+ std = np.std(X, axis=(1), keepdims=True)
186
+ print(X.shape)
187
+
188
+ # Standardize the data
189
+ X_standardized = (X - mean) / std
190
+
191
+ return X_standardized
192
+
193
+
194
+ # Compute Correlation Matrix
195
+ def compute_correlation_matrix(X: np.ndarray) -> np.ndarray:
196
+ """
197
+ Compute the correlation matrix for the data.
198
+
199
+ Parameters:
200
+ -----------
201
+ X : np.ndarray
202
+ Input data of shape (n_samples, n_segments, n_channels, n_time_samples)
203
+
204
+ Returns:
205
+ --------
206
+ np.ndarray
207
+ Correlation matrices of shape (n_samples, n_segments, n_channels, n_channels)
208
+ """
209
+ # Declare corr_matrix np array of shape (n_samples, n_segments,n_channels, n_channels)
210
+ corr_matrix = np.zeros((X.shape[0], X.shape[1], X.shape[1]))
211
+ for j in range(X.shape[0]): # for each segment 5 secs
212
+ # Compute the correlation matrix
213
+ temp = np.corrcoef(X[j])
214
+ corr_matrix[j] = np.nan_to_num(temp)
215
+
216
+ return corr_matrix
217
+
218
+
219
+
220
+ # Discard bottom triangle from the matrix:
221
+ def discard_bottom_triangle(matrix):
222
+ """
223
+ Discard the bottom triangle of a square matrix.
224
+
225
+ Parameters:
226
+ - matrix (numpy.ndarray): The input square matrix.
227
+
228
+ Returns:
229
+ - numpy.ndarray: The matrix with the bottom triangle discarded.
230
+ """
231
+ # Create a mask for the upper triangle
232
+ mask = np.triu(np.ones_like(matrix, dtype=bool), k=1)
233
+
234
+ # Apply the mask to the matrix
235
+ upper_triangle = np.where(mask, matrix, 0)
236
+
237
+ return upper_triangle
238
+
239
+ def extract_upper_triangle(corr_matrices):
240
+ """
241
+ Extract upper triangles from correlation matrices
242
+
243
+ Args:
244
+ corr_matrices: numpy array of shape (n_sample, n_segments, n_channels, n_channels)
245
+
246
+ Returns:
247
+ numpy array of shape (n_segments, n_features) where n_features = n_channels*(n_channels-1)/2
248
+ """
249
+ n_segments, n_channels, = corr_matrices.shape[0], corr_matrices.shape[1]
250
+ n_features = n_channels * (n_channels - 1) // 2
251
+
252
+ flattened = np.zeros((n_segments, n_features))
253
+
254
+ for j in range(n_segments):
255
+ # Get upper triangle indices (excluding diagonal)
256
+ upper_indices = np.triu_indices(n_channels, k=1)
257
+ # Extract values
258
+ flattened[j] = corr_matrices[j][upper_indices]
259
+
260
+ return flattened