Spaces:
Sleeping
Sleeping
Upload 11 files
Browse files- Dockerfile +27 -0
- __pycache__/app.cpython-312.pyc +0 -0
- __pycache__/prediction.cpython-312.pyc +0 -0
- __pycache__/preprocessing.cpython-312.pyc +0 -0
- __pycache__/preprocessing_2dcnn.cpython-312.pyc +0 -0
- app.py +58 -0
- model1_2dcnn.h5 +3 -0
- prediction.py +34 -0
- preprocessing.py +96 -0
- preprocessing_2dcnn.py +52 -0
- requirements.txt +10 -0
Dockerfile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM continuumio/miniconda3
|
| 2 |
+
|
| 3 |
+
# Update apt-get and install basic utilities
|
| 4 |
+
RUN apt-get update -y && apt-get install -y nano unzip curl
|
| 5 |
+
|
| 6 |
+
# THIS IS SPECIFIC TO HUGGINGFACE: Create a non-root user
|
| 7 |
+
RUN useradd -m -u 1000 user
|
| 8 |
+
|
| 9 |
+
USER user
|
| 10 |
+
ENV HOME=/home/user
|
| 11 |
+
ENV PATH=/home/user/.local/bin:$PATH
|
| 12 |
+
|
| 13 |
+
# Set working directory
|
| 14 |
+
WORKDIR $HOME/app
|
| 15 |
+
|
| 16 |
+
# Copy only requirements file first to leverage Docker caching
|
| 17 |
+
COPY --chown=user requirements.txt /dependencies/requirements.txt
|
| 18 |
+
|
| 19 |
+
# Install Python dependencies from requirements.txt
|
| 20 |
+
RUN pip install --no-cache-dir -r /dependencies/requirements.txt
|
| 21 |
+
|
| 22 |
+
# Copy the rest of the application code
|
| 23 |
+
COPY --chown=user . $HOME/app
|
| 24 |
+
|
| 25 |
+
EXPOSE 7860
|
| 26 |
+
|
| 27 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
__pycache__/app.cpython-312.pyc
ADDED
|
Binary file (3.12 kB). View file
|
|
|
__pycache__/prediction.cpython-312.pyc
ADDED
|
Binary file (2.24 kB). View file
|
|
|
__pycache__/preprocessing.cpython-312.pyc
ADDED
|
Binary file (4.15 kB). View file
|
|
|
__pycache__/preprocessing_2dcnn.cpython-312.pyc
ADDED
|
Binary file (2.49 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import tempfile
|
| 3 |
+
import uvicorn
|
| 4 |
+
from fastapi import FastAPI, UploadFile, File, HTTPException
|
| 5 |
+
from fastapi.responses import JSONResponse
|
| 6 |
+
import tensorflow as tf
|
| 7 |
+
|
| 8 |
+
# Import your prediction function from your prediction module.
|
| 9 |
+
from prediction import predict_eeg_recording
|
| 10 |
+
|
| 11 |
+
app = FastAPI(title="EEG Epilepsy Prediction API")
|
| 12 |
+
|
| 13 |
+
# Load the trained model once at startup.
|
| 14 |
+
model = tf.keras.models.load_model('model1_2dcnn.h5')
|
| 15 |
+
|
| 16 |
+
@app.get("/", tags=["Introduction Endpoints"])
|
| 17 |
+
async def index():
|
| 18 |
+
"""
|
| 19 |
+
Simply returns a welcome message!
|
| 20 |
+
"""
|
| 21 |
+
message = (
|
| 22 |
+
"Hello world! Welcome to the EEG Epilepsy Prediction API. "
|
| 23 |
+
"Submit an EEG recording EDF file to the `/predict` endpoint to receive a prediction."
|
| 24 |
+
)
|
| 25 |
+
return message
|
| 26 |
+
|
| 27 |
+
@app.post("/predict", tags=["Machine Learning"])
|
| 28 |
+
async def predict_endpoint(file: UploadFile = File(...)):
|
| 29 |
+
"""
|
| 30 |
+
Accepts an EEG EDF file, processes it using the preprocessing and spectrogram conversion functions,
|
| 31 |
+
and returns an aggregated prediction (epilepsy or no epilepsy) along with the mean probability.
|
| 32 |
+
"""
|
| 33 |
+
# Save the uploaded file temporarily.
|
| 34 |
+
try:
|
| 35 |
+
suffix = os.path.splitext(file.filename)[1]
|
| 36 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
|
| 37 |
+
tmp.write(await file.read())
|
| 38 |
+
tmp_path = tmp.name
|
| 39 |
+
except Exception as e:
|
| 40 |
+
raise HTTPException(status_code=500, detail="Error saving temporary file")
|
| 41 |
+
|
| 42 |
+
# Use your prediction function to process the file and obtain an aggregated prediction.
|
| 43 |
+
try:
|
| 44 |
+
pred_label, mean_prob = predict_eeg_recording(tmp_path, model, threshold=0.5)
|
| 45 |
+
except Exception as e:
|
| 46 |
+
os.remove(tmp_path)
|
| 47 |
+
raise HTTPException(status_code=400, detail=f"Prediction failed: {e}")
|
| 48 |
+
|
| 49 |
+
os.remove(tmp_path)
|
| 50 |
+
|
| 51 |
+
response = {
|
| 52 |
+
"prediction": "epilepsy" if pred_label == 1 else "no epilepsy",
|
| 53 |
+
"mean_probability": float(mean_prob)
|
| 54 |
+
}
|
| 55 |
+
return JSONResponse(content=response)
|
| 56 |
+
|
| 57 |
+
if __name__ == "__main__":
|
| 58 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
model1_2dcnn.h5
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7667171590e5ee5d9a189a22f454b08ec8d5dfcdfd42b9dda4c96ea0f32c07f3
|
| 3 |
+
size 1679000
|
prediction.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import tensorflow as tf
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from sklearn.metrics import confusion_matrix, f1_score, accuracy_score
|
| 5 |
+
from preprocessing import preprocess_eeg_file
|
| 6 |
+
from preprocessing_2dcnn import convert_epoch_to_spectrogram
|
| 7 |
+
import random
|
| 8 |
+
random.seed(42)
|
| 9 |
+
|
| 10 |
+
def aggregate_predictions(spectrogram_list, model, threshold=0.5):
|
| 11 |
+
|
| 12 |
+
# Convert each spectrogram to channels-last format.
|
| 13 |
+
X = np.array([np.transpose(s, (1, 2, 0)) for s in spectrogram_list])
|
| 14 |
+
print(f'---Aggregating predictions from {len(spectrogram_list)} segments---')
|
| 15 |
+
preds = model.predict(X)
|
| 16 |
+
mean_prob = np.mean(preds[:, 1])
|
| 17 |
+
final_label = 1 if mean_prob >= threshold else 0
|
| 18 |
+
return final_label, mean_prob
|
| 19 |
+
|
| 20 |
+
def predict_eeg_recording(edf_path, model, threshold=0.5):
|
| 21 |
+
#Process the edf file
|
| 22 |
+
preprocessed_df = preprocess_eeg_file(edf_path, fmin=1.0, fmax=45.0, segment_lenght=5, overlap=2)
|
| 23 |
+
|
| 24 |
+
if preprocessed_df is None or preprocessed_df.empty:
|
| 25 |
+
raise ValueError("EEG file could not be preprocessed or no valid segments found.")
|
| 26 |
+
|
| 27 |
+
channels = ["EEG FP1-REF", "EEG FP2-REF", "EEG F3-REF", "EEG F4-REF", "EEG C3-REF"]
|
| 28 |
+
# Convert each 5-second segment (each row) into a spectrogram.
|
| 29 |
+
spectrogram_list = preprocessed_df.apply(
|
| 30 |
+
lambda row: convert_epoch_to_spectrogram(row, channels, fs=250, nperseg=128, noverlap=64), axis=1
|
| 31 |
+
).tolist()
|
| 32 |
+
|
| 33 |
+
return aggregate_predictions(spectrogram_list, model, threshold)
|
| 34 |
+
|
preprocessing.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
def select_relevant_channels(raw):
|
| 23 |
+
# For relevant channel criteria check documentation
|
| 24 |
+
'''“EEG FP1-REF” for the left frontal pole
|
| 25 |
+
|
| 26 |
+
“EEG FP2-REF” for the right frontal pole
|
| 27 |
+
|
| 28 |
+
“EEG F3-REF” for the left frontal region
|
| 29 |
+
|
| 30 |
+
“EEG F4-REF” for the right frontal region
|
| 31 |
+
|
| 32 |
+
“EEG C3-REF” for the left central region'''
|
| 33 |
+
|
| 34 |
+
desired = ["EEG FP1-REF", "EEG FP2-REF", "EEG F3-REF", "EEG F4-REF", "EEG C3-REF"]
|
| 35 |
+
#check if all desired channels are present; if not, skip this file
|
| 36 |
+
if not all(ch in raw.ch_names for ch in desired):
|
| 37 |
+
print("Skipping file because it doesn't have the full set of desired channels.")
|
| 38 |
+
return None
|
| 39 |
+
raw.pick_channels(desired, verbose=False)
|
| 40 |
+
return raw
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def collapse_epoch_df_by_channel(epoch_df):
|
| 44 |
+
# Identify channel columns (exclude time, epoch, condition)
|
| 45 |
+
channel_cols = [col for col in epoch_df.columns if col not in ['time', 'epoch']]
|
| 46 |
+
# Group by epoch
|
| 47 |
+
grouped = epoch_df.groupby('epoch')
|
| 48 |
+
rows = []
|
| 49 |
+
for epoch_num, group in grouped:
|
| 50 |
+
group_sorted = group.sort_values('time')
|
| 51 |
+
# For each channel, extract the 1D array for this epoch
|
| 52 |
+
row = {'epoch': epoch_num}
|
| 53 |
+
for ch in channel_cols:
|
| 54 |
+
row[ch] = group_sorted[ch].values # 1D array of length = number of time samples in the epoch
|
| 55 |
+
rows.append(row)
|
| 56 |
+
return pd.DataFrame(rows)
|
| 57 |
+
|
| 58 |
+
def preprocess_eeg_file(edf_path, fmin=1.0, fmax=45.0, segment_lenght=5, overlap=2):
|
| 59 |
+
|
| 60 |
+
# 1. Charger le fichier EDF avec MNE
|
| 61 |
+
raw = mne.io.read_raw_edf(edf_path, preload=True, verbose=False)
|
| 62 |
+
|
| 63 |
+
# Resample (to 250 because it's the lowest sampling rate )
|
| 64 |
+
raw.resample(250, verbose=False)
|
| 65 |
+
|
| 66 |
+
# Filtrage passe-bande (1-45 Hz)
|
| 67 |
+
raw.filter(fmin, fmax, fir_design='firwin', verbose=False)
|
| 68 |
+
|
| 69 |
+
# Skip EEGs less than 5s
|
| 70 |
+
if raw.times[-1] < 5:
|
| 71 |
+
print(f"Skipping {edf_path}: duration ({raw.times[-1]:.2f} s) is less than required 5s.")
|
| 72 |
+
return None
|
| 73 |
+
|
| 74 |
+
# Suppression des canaux non EEG
|
| 75 |
+
eeg_channels = mne.pick_types(raw.info, eeg=True, exclude=[])
|
| 76 |
+
raw.pick(eeg_channels, verbose=False)
|
| 77 |
+
|
| 78 |
+
# Selectionner les channels pertinents (channel selection from EDA ?)
|
| 79 |
+
print(raw.ch_names)
|
| 80 |
+
raw = select_relevant_channels(raw)
|
| 81 |
+
if raw is None:
|
| 82 |
+
return None
|
| 83 |
+
|
| 84 |
+
# Segmentation
|
| 85 |
+
epochs = mne.make_fixed_length_epochs(raw, duration=segment_lenght, preload=False, overlap=overlap, verbose=False)
|
| 86 |
+
|
| 87 |
+
# Transform to dataframe and standadize
|
| 88 |
+
|
| 89 |
+
df = epochs.to_data_frame() # epochs is returned by preprocess_eeg_file()
|
| 90 |
+
df_std = standardize_dataframe(df.drop(['time','epoch', 'condition'], axis=1))
|
| 91 |
+
result = pd.concat([df[['time','epoch']], df_std], axis=1)
|
| 92 |
+
|
| 93 |
+
return collapse_epoch_df_by_channel(result)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
|
preprocessing_2dcnn.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
from scipy.signal import spectrogram
|
| 4 |
+
|
| 5 |
+
#METHOD 1: STFT spectrogram
|
| 6 |
+
def convert_epoch_to_spectrogram(epoch_row, channels, fs=250, nperseg=128, noverlap=64):
|
| 7 |
+
"""
|
| 8 |
+
Given a pandas Series representing one epoch, where each channel column contains
|
| 9 |
+
a 1D numpy array of time series data, compute a spectrogram for each channel and
|
| 10 |
+
stack them into a 3D array of shape (n_channels, freq_bins, time_bins).
|
| 11 |
+
|
| 12 |
+
Parameters:
|
| 13 |
+
epoch_row: pandas Series
|
| 14 |
+
One row of your preprocessed dataframe (one epoch).
|
| 15 |
+
channels: list of str
|
| 16 |
+
The list of channel names to process.
|
| 17 |
+
fs: int
|
| 18 |
+
Sampling frequency (default 250 Hz).
|
| 19 |
+
nperseg: int
|
| 20 |
+
Length of each segment for spectrogram calculation.
|
| 21 |
+
noverlap: int
|
| 22 |
+
Number of overlapping samples between segments.
|
| 23 |
+
|
| 24 |
+
Returns:
|
| 25 |
+
spec_stack: numpy array
|
| 26 |
+
A 3D array with shape (n_channels, freq_bins, time_bins) containing the
|
| 27 |
+
spectrogram (in dB) for each channel.
|
| 28 |
+
"""
|
| 29 |
+
spec_list = []
|
| 30 |
+
for ch in channels:
|
| 31 |
+
ts = epoch_row[ch] # this is the 1D time series for the channel
|
| 32 |
+
# Compute the spectrogram
|
| 33 |
+
f, t, Sxx = spectrogram(ts, fs=fs, nperseg=nperseg, noverlap=noverlap)
|
| 34 |
+
# Convert the power spectrogram to dB scale
|
| 35 |
+
Sxx_db = 10 * np.log10(Sxx + 1e-10)
|
| 36 |
+
spec_list.append(Sxx_db)
|
| 37 |
+
spec_stack = np.stack(spec_list, axis=0)
|
| 38 |
+
return spec_stack
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def convert_preprocessed_df_to_2d(preprocessed_df, channels=["EEG FP1-REF", "EEG FP2-REF", "EEG F3-REF", "EEG F4-REF", "EEG C3-REF"],
|
| 42 |
+
fs=250, nperseg=128, noverlap=64):
|
| 43 |
+
|
| 44 |
+
# Make a copy to avoid modifying the original dataframe
|
| 45 |
+
df = preprocessed_df.copy()
|
| 46 |
+
|
| 47 |
+
# Compute the spectrogram for each row (epoch)
|
| 48 |
+
df["spectrogram"] = df.apply(lambda row: convert_epoch_to_spectrogram(row, channels, fs, nperseg, noverlap), axis=1)
|
| 49 |
+
|
| 50 |
+
#drop channel columns and other useless ones
|
| 51 |
+
df_final = df[["spectrogram", "epilepsy", "age", "gender",'subject_id', 'edf_path']]
|
| 52 |
+
return df_final
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy==1.26.4
|
| 2 |
+
pandas==2.2.3
|
| 3 |
+
uvicorn==0.34.0
|
| 4 |
+
scipy==1.15.0
|
| 5 |
+
tensorflow==2.16.2
|
| 6 |
+
scikit-learn==1.6.1
|
| 7 |
+
fastapi==0.115.12
|
| 8 |
+
mne==1.9.0
|
| 9 |
+
python-multipart==0.0.20
|
| 10 |
+
|