MorganBrizon's picture
Update app.py
268a506 verified
Raw
History Blame Contribute Delete
20.9 kB
import streamlit as st
import mne
import tempfile
import os
import matplotlib.pyplot as plt
import plotly.graph_objs as go
import numpy as np
from datetime import datetime
from fpdf import FPDF
import pathlib
import requests
from scipy import signal
import google.generativeai as genai
import pandas as pd
from gemini_report import generate_full_llm_report
# Configuration de la page
st.set_page_config(page_title="🧠 Application de Détection de l’Épilepsie par EEG", layout="wide")
# ✅ Fond d'écran personnalisé
BACKGROUND_URL = "https://img.freepik.com/vector-gratis/fondo-medico-sanitario-espacio-texto_1017-26841.jpg"
def set_background_from_url(url):
css = f"""
<style>
html, body {{
height: 100%;
margin: 0;
padding: 0;
background-image: url('{url}');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
}}
.stApp {{
background-color: rgba(255, 255, 255, 0.85);
}}
</style>
"""
st.markdown(css, unsafe_allow_html=True)
set_background_from_url(BACKGROUND_URL)
# ==============================
# Interface à onglets
# ==============================
tabs = st.tabs([
"🏠 Accueil",
"📈 Visualisation EEG",
"🔎 Prédiction",
"📋 Résultat Clinique",
"📝 Rapport LLM (Gemini)",
"❓ À propos"
])
# Variables de session
if "raw" not in st.session_state:
st.session_state.raw = None
if "filename" not in st.session_state:
st.session_state.filename = None
if "tmp_path" not in st.session_state:
st.session_state.tmp_path = None
if "prediction_result" not in st.session_state:
st.session_state.prediction_result = None
# ==============================
# Onglet Accueil
# ==============================
with tabs[0]:
st.title("🧠 Application de Détection de l’Épilepsie par EEG")
st.markdown("""
### 🏥 Outil d’Aide à la Décision Clinique
Basé sur l’analyse automatique des signaux EEG
### 🧬 Description / Contexte médical
Cette application est un système d’aide à la décision clinique conçu pour assister les professionnels de santé dans l’interprétation d’enregistrements EEG dans le cadre du dépistage et du diagnostic de l’épilepsie.
Elle repose sur des algorithmes d’apprentissage automatique entraînés sur des milliers de segments EEG annotés (Temple University Hospital), capables de prédire la probabilité qu’un enregistrement soit associé à une activité épileptiforme.
⚠️ Ce système n’est pas un dispositif médical certifié. Son utilisation est réservée aux professionnels de santé, comme outil complémentaire à l’analyse clinique.
""")
# ==============================
# Onglet Visualisation EEG
# ==============================
with tabs[1]:
st.header("📈 Visualisation des signaux EEG")
uploaded_file = st.file_uploader("📄 Charger un fichier .edf", type=["edf"])
if uploaded_file is not None:
with tempfile.NamedTemporaryFile(delete=False, suffix=".edf") as tmp:
tmp.write(uploaded_file.read())
tmp_path = tmp.name
try:
raw = mne.io.read_raw_edf(tmp_path, preload=True)
st.session_state.raw = raw
st.session_state.filename = uploaded_file.name
st.session_state.tmp_path = tmp_path
st.success("✅ Fichier EDF chargé avec succès")
except Exception as e:
st.error(f"❌ Erreur : {e}")
raw = st.session_state.raw
if raw is not None:
st.write(f"**Fichier :** {st.session_state.filename}")
st.write(f"**Nombre de canaux :** {len(raw.ch_names)}")
st.write(f"**Durée :** {raw.times[-1] / 60:.2f} minutes")
viz_type = st.selectbox("🎨 Choisir le type de visualisation",
["Multi-canaux (Plotly)", "Canal unique", "Spectrogramme"])
total_duration = raw.times[-1]
window_duration = 30
start_time = st.slider("⏱️ Position de départ (sec)", 0.0, float(max(0.0, total_duration - window_duration)), 0.0, 1.0)
if viz_type == "Multi-canaux (Plotly)":
selected_channels = st.multiselect("Sélectionner les canaux (max 5)", raw.ch_names, default=raw.ch_names[:5])
if len(selected_channels) > 5:
st.warning("⚠️ Veuillez sélectionner 5 canaux maximum.")
elif selected_channels:
try:
raw_segment = raw.copy().pick_channels(selected_channels).crop(tmin=start_time, tmax=start_time + window_duration)
data, times = raw_segment.get_data(return_times=True)
traces = []
for i, channel in enumerate(selected_channels):
traces.append(go.Scatter(x=times, y=data[i], mode="lines", name=channel))
fig = go.Figure(data=traces)
fig.update_layout(title="EEG multi-canaux", xaxis_title="Temps (s)", yaxis_title="Amplitude (µV)")
st.plotly_chart(fig)
except Exception as e:
st.error(f"Erreur affichage multi-canaux : {e}")
elif viz_type == "Canal unique":
selected_channel = st.selectbox("Choisir un canal EEG", raw.ch_names)
try:
raw_segment = raw.copy().pick_channels([selected_channel]).crop(tmin=start_time, tmax=start_time + window_duration)
data, times = raw_segment.get_data(return_times=True)
trace = go.Scatter(x=times, y=data[0], mode="lines", name=selected_channel)
fig = go.Figure(data=[trace])
fig.update_layout(title=f"EEG - {selected_channel}", xaxis_title="Temps (s)", yaxis_title="Amplitude (µV)")
st.plotly_chart(fig)
except Exception as e:
st.error(f"Erreur affichage canal unique : {e}")
elif viz_type == "Spectrogramme":
selected_channel = st.selectbox("Choisir un canal pour le spectrogramme", raw.ch_names)
try:
raw_segment = raw.copy().pick_channels([selected_channel]).crop(tmin=start_time, tmax=start_time + window_duration)
data, times = raw_segment.get_data(return_times=True)
fs = int(raw.info['sfreq'])
f, t, Sxx = signal.spectrogram(data[0], fs)
fig, ax = plt.subplots(figsize=(7, 3.5))
im = ax.pcolormesh(t, f, 10 * np.log10(Sxx), shading='gouraud')
ax.set_ylabel('Fréquence [Hz]')
ax.set_xlabel('Temps [s]')
ax.set_title(f"Spectrogramme : {selected_channel}")
plt.colorbar(im, ax=ax, label='dB')
st.pyplot(fig)
st.markdown("""
---
### 🧠 Légende - Signification clinique des bandes de fréquence EEG
| Bande (Hz) | Nom | État physiologique typique | Indication clinique |
|------------|----------|-----------------------------|----------------------|
| 0.5–4 | Delta | Sommeil profond | Présente éveillé = suspicion de pathologie |
| 4–8 | Theta | Somnolence, relaxation | Exagéré dans certaines pathologies |
| 8–13 | Alpha | Éveil calme (yeux fermés) | Disparaît yeux ouverts |
| 13–30 | Beta | Concentration, éveil actif | Accentué sous stress ou médication |
| >30 | Gamma | Activité cognitive | Rarement analysée en clinique |
ℹ️ Le spectrogramme permet de visualiser les variations fréquentielles du signal EEG dans le temps. Il est particulièrement utile pour détecter des décharges anormales ou des crises.
""")
except Exception as e:
st.error(f"Erreur affichage spectrogramme : {e}")
st.markdown("""
#### ℹ️ Remarque
Ces métriques ont été obtenues à partir de segments EEG de patients annotés manuellement (crises, artefacts, normalité).
Le modèle est optimisé pour une agrégation des segments à l’échelle patient, permettant une prédiction robuste au niveau global.
""")
else:
st.info("Veuillez charger un fichier EDF ci-dessus.")
# ==============================
# Onglet Prédiction
# ==============================
with tabs[2]:
st.header("🔎 Prédiction via API externe")
raw = st.session_state.raw
tmp_path = st.session_state.tmp_path
if raw is not None and tmp_path is not None:
# ✅ Mapping affichage utilisateur → valeur envoyée à l'API
model_mapping = {
"🧠 Modèle 1 (CNN 2D)": "2DCNN",
"⚡ Modèle 2 (EEGNet)": "EEGNet",
"🌀 Modèle 3 (EpilepsyNet)": "EpilepsyNet",
}
model_display = st.selectbox("🧠 Choisir le modèle de prédiction", list(model_mapping.keys()))
selected_model = model_mapping[model_display]
if st.button("📡 Lancer la prédiction"):
try:
api_url = "https://MorganBrizon-EEG-API.hf.space/predict"
# Préparation du fichier
with open(tmp_path, "rb") as f:
files = {"file": (st.session_state.filename, f, "application/octet-stream")}
# ✅ Préparation des bons paramètres
if selected_model == "ensemble_avg":
params = {"model_choice": "ensemble", "ensemble_method": "average"}
elif selected_model == "ensemble_vote":
params = {"model_choice": "ensemble", "ensemble_method": "voting"}
else:
params = {"model_choice": selected_model}
# Envoi vers l’API
response = requests.post(api_url, files=files, params=params)
if response.status_code != 200:
st.error(f"❌ Erreur API : {response.status_code} - {response.text}")
st.stop()
result = response.json()
label = result.get("prediction")
prob = result.get("mean_probability")
segment_probs = result.get("segment_probabilities", [])
if label == "no epilepsy": prob = 1-prob
# Enregistrement en session
st.session_state.prediction_result = (label, prob)
st.session_state.segment_probs = segment_probs
st.success("✅ Prédiction reçue")
st.write(f"🧠 Résultat global : **{label}**")
st.write(f"📊 Probabilité moyenne : **{prob * 100:.2f}%**")
if prob < 0.6:
st.warning("🔍 Confiance : Faible")
elif prob < 0.85:
st.info("✅ Confiance : Moyenne")
else:
st.success("💪 Confiance : Élevée")
# Tableau et graphe
if segment_probs:
st.markdown("### 📋 Prédictions par segment EEG")
import pandas as pd
import matplotlib.pyplot as plt
segment_length = 5
overlap = 2
step = segment_length - overlap
df = pd.DataFrame({
"Segment (sec)": [f"{i * step}-{i * step + segment_length}s" for i in range(len(segment_probs))],
"Probabilité (%)": [round(p * 100, 2) for p in segment_probs],
"Étiquette": ["épilepsie" if p > 0.5 else "non" for p in segment_probs]
})
st.dataframe(df, use_container_width=True)
total_seconds = (len(segment_probs) - 1) * step + segment_length
minutes = total_seconds // 60
seconds = total_seconds % 60
st.write(f"🧮 Nombre de segments : **{len(segment_probs)}**")
st.write(f"⏱️ Durée EEG analysée : **{minutes} min {seconds} sec**")
st.markdown("### 📊 Visualisation graphique des segments")
fig, ax = plt.subplots(figsize=(12, 3))
colors = ['red' if p > 0.5 else 'green' for p in segment_probs]
ax.bar(range(len(segment_probs)), segment_probs, color=colors)
ax.set_xticks(range(0, len(segment_probs), max(1, len(segment_probs) // 20)))
ax.set_xticklabels(
[f"{i * step}-{i * step + segment_length}s" for i in range(0, len(segment_probs), max(1, len(segment_probs) // 20))],
rotation=45
)
ax.set_ylim(0, 1)
ax.set_ylabel("Probabilité")
ax.set_title("Probabilité d’épilepsie par segment (fenêtres de 5s, pas de 3s)")
st.pyplot(fig)
st.caption("🔴 = épilepsie (p > 0.5) · 🟢 = non épileptique")
else:
st.warning("⚠️ Aucune prédiction segmentée reçue.")
except Exception as e:
st.error(f"Erreur API : {e}")
else:
st.info("Veuillez charger un fichier EDF dans l'onglet '📈 Visualisation EEG'.")
# ==============================
# Onglet Résultat Clinique
# ==============================
with tabs[3]:
st.header("📋 Rapport Clinique")
if st.session_state.prediction_result and st.session_state.raw:
label, prob = st.session_state.prediction_result
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
st.write(f"**Identifiant patient :** {st.session_state.filename}")
st.write(f"**Date et heure :** {now}")
st.write(f"**Résultat prédiction :** {label}")
if prob is not None:
st.write(f"**Probabilité :** {prob * 100:.2f}%")
st.markdown("### 🧪 Interprétation")
if "non" in label.lower():
st.info("Le modèle ne détecte pas de signes épileptiques caractéristiques.")
texte_interpretation = "Le modele ne detecte pas de signes epileptiques caracteristiques."
else:
st.warning("Le modèle détecte des signes compatibles avec une activité épileptique.")
texte_interpretation = "Le modele detecte des signes compatibles avec une activite epileptique."
st.markdown("### ⚠️ Mentions légales")
st.caption("""
Cet outil d’aide à la décision clinique ne constitue pas un diagnostic médical.
Il ne doit être utilisé qu’en complément de l’évaluation médicale complète.
""")
# 📥 Génération du rapport PDF
pdf = FPDF()
pdf.add_page()
pdf.set_auto_page_break(auto=True, margin=15)
font_path = str(pathlib.Path(__file__).parent / "DejaVuSans.ttf")
try:
pdf.add_font("DejaVu", "", font_path, uni=True)
except RuntimeError:
st.error("❌ Impossible de charger la police DejaVuSans.ttf. Place-la dans le même dossier que app.py.")
st.stop()
pdf.set_font("DejaVu", size=12)
pdf.cell(200, 10, txt="Rapport d'analyse EEG", ln=True, align="C")
pdf.ln(10)
pdf.cell(200, 10, txt=f"Identifiant patient : {st.session_state.filename}", ln=True)
pdf.cell(200, 10, txt=f"Date et heure de l’analyse : {now}", ln=True)
pdf.cell(200, 10, txt=f"Résultat de la prédiction : {label}", ln=True)
if prob is not None:
pdf.cell(200, 10, txt=f"Probabilité : {prob * 100:.2f}%", ln=True)
pdf.ln(10)
pdf.multi_cell(0, 10, txt="Interprétation :")
texte_interpretation = texte_interpretation.encode('ascii', 'ignore').decode()
pdf.multi_cell(w=190, h=10, txt=texte_interpretation)
pdf.ln(10)
pdf.multi_cell(0, 10, txt="Mentions légales :")
pdf.set_font("DejaVu", size=10)
texte_legal = "Cet outil d'aide a la decision clinique ne constitue pas un diagnostic medical. Il ne doit etre utilise qu'en complement de l'evaluation medicale complete."
texte_legal = texte_legal.encode('ascii', 'ignore').decode()
pdf.multi_cell(w=190, h=10, txt=texte_legal)
pdf.ln(5)
pdf.set_font("DejaVu", size=10)
remarque = "Ces metriques ont ete obtenues a partir de segments EEG de patients annotes manuellement (crises, artefacts, normalite). Le modele est optimise pour une aggregation des segments a l'echelle patient, permettant une prediction robuste au niveau global."
remarque = remarque.encode('ascii', 'ignore').decode()
pdf.multi_cell(w=190, h=10, txt=remarque)
pdf_output_path = os.path.join(tempfile.gettempdir(), "rapport_eeg.pdf")
pdf.output(pdf_output_path)
with open(pdf_output_path, "rb") as f:
st.download_button(label="📥 Télécharger le rapport PDF", data=f, file_name="rapport_eeg.pdf")
else:
st.info("Lancez une prédiction dans l'onglet 'Prédiction'.")
# ==============================
# Onglet Rapport par LLM (Gemini)
# ==============================
if "GEMINI_API_KEY" in st.secrets:
genai.configure(api_key=st.secrets["GEMINI_API_KEY"])
else:
st.warning("⚠️ Clé Gemini manquante dans secrets.toml")
from gemini_report import generate_full_llm_report
with tabs[4]:
st.header("🖍️ Rapport Clinique via LLM (Gemini)")
raw = st.session_state.get("raw", None)
tmp_path = st.session_state.get("tmp_path", None)
filename = st.session_state.get("filename", None)
if raw and tmp_path and filename:
if st.button("🧠 Générer le rapport avec Gemini"):
with st.spinner("Génération du rapport en cours..."):
# 👤 Métadonnées fictives (non affichées dans l'interface)
metadata = {
"subject_id": "Unknown",
"age": "Unknown",
"gender": "Unknown",
"epilepsy": "Unknown"
}
try:
report = generate_full_llm_report(tmp_path, metadata)
st.markdown("### 💾 Rapport EEG généré :")
st.text_area("🧾 Rapport médical structuré", report, height=500)
except Exception as e:
st.error(f"❌ Erreur pendant la génération du rapport : {e}")
else:
st.info("📂 Veuillez charger un fichier EDF dans l’onglet '📈 Visualisation EEG'.")
# ==============================
# Onglet À propos
# ==============================
with tabs[5]:
st.title("❓ À propos de cet outil")
st.markdown("""
🧠 **À propos de cet outil :**
Cet outil repose sur des techniques d’apprentissage profond (Deep Learning) pour détecter automatiquement des patterns épileptiformes dans des enregistrements EEG cliniques.
Le modèle a été entraîné et validé sur des enregistrements annotés du TUH EEG corpus (Temple University Epilepsy Corpus), largement utilisé dans la recherche sur l’épilepsie.
🔬 **Méthodologie :**
- **Prétraitement :**
- Sélection de 5 canaux EEG standardisés (EEG FP1-REF, EEG FP2-REF, EEG F3-REF, EEG F4-REF, EEG C3-REF)
- Filtrage passe-bande (1–45 Hz)
- Rééchantillonnage à 250 Hz
- Normalisation canal par canal
- Segmentation des enregistrements en fenêtres de 5 secondes
- **Architecture du modèle :**
- 2D-CNN entraîné à partir de segments EEG transformés en matrices 2D (canal × temps)
- Couches convolutives extrayant des motifs spatio-temporels liés à l’activité épileptiforme
- Couches fully connected pour la classification binaire (épileptique / non-épileptique)
- Fonction de perte : Binary Cross-Entropy
- Optimisation : Adam
- **Évaluation & sortie :**
- Prédictions réalisées au niveau des segments EEG
- Agrégation des prédictions pour produire un score probabiliste patient-level
- Retour d’une probabilité d’épilepsie (%), accompagnée d’une interprétation localisée
⚠️ **Limites de l’outil :**
- Moins fiable sur :
- Signaux de faible qualité ou fortement bruités
- Formes rares, focales profondes ou non typiques d’épilepsie
- Ne remplace pas l’expertise médicale humaine (neurologue)
""")