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""" """ 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) """)