Ranjith445's picture
Upload app.py
9ee4afa verified
Raw
History Blame Contribute Delete
55.3 kB
import warnings
warnings.filterwarnings("ignore")
import re
import os
import json
import numpy as np
import pickle
import streamlit as st
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from matplotlib.patches import Circle, FancyArrowPatch
from scipy.signal import welch
from scipy.interpolate import griddata
# MNE imported lazily only when needed for upload mode
import tempfile
import io
from PIL import Image as PILImage
from reportlab.lib.pagesizes import A4, landscape
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image, PageBreak
from reportlab.lib.units import cm
# ─────────────────────────────────────────
# CONFIG
# ─────────────────────────────────────────
st.set_page_config(page_title="EEG Cognitive Analyzer", page_icon="🧠", layout="wide")
MODELS_DIR = "models"
SAMPLES_DIR = "samples"
KEY_CHANNELS = ['Fz', 'C3', 'Cz', 'C4', 'Pz', 'F3', 'F4', 'O1', 'O2']
BANDS = {'theta': (4, 8), 'alpha': (8, 13), 'beta': (13, 30)}
LOW_FREQ = 1.0
HIGH_FREQ = 40.0
EPOCH_LEN = 2.0
# Approximate 2D electrode positions (x, y) for topomap
CHANNEL_POS = {
'Fz': (0.0, 0.5),
'F3': (-0.4, 0.4),
'F4': (0.4, 0.4),
'C3': (-0.5, 0.0),
'Cz': (0.0, 0.0),
'C4': (0.5, 0.0),
'Pz': (0.0, -0.5),
'O1': (-0.3,-0.7),
'O2': (0.3, -0.7),
}
BAND_BIOLOGY = {
'theta': {'color':'#7c3aed','emoji':'🟣','role':'Memory & Mental Fatigue','range':'4–8 Hz'},
'alpha': {'color':'#2563eb','emoji':'🔵','role':'Relaxation & Idle Rhythm','range':'8–13 Hz'},
'beta': {'color':'#dc2626','emoji':'🔴','role':'Active Thinking & Motor Control','range':'13–30 Hz'},
}
# ─────────────────────────────────────────
# LOAD MODELS
# ─────────────────────────────────────────
@st.cache_resource
def load_models():
with open(os.path.join(MODELS_DIR, "brain_state_model.pkl"), 'rb') as f:
state_model = pickle.load(f)
with open(os.path.join(MODELS_DIR, "cognitive_load_model.pkl"), 'rb') as f:
load_model = pickle.load(f)
return state_model, load_model
@st.cache_data
def load_sample_meta():
# Try multiple possible locations
for base in [SAMPLES_DIR, "samples", "/app/samples", "."]:
meta_path = os.path.join(base, "sample_meta.json")
if os.path.exists(meta_path):
with open(meta_path) as f:
meta = json.load(f)
# Always normalize image paths
for item in meta:
subject = item['subject']
for base2 in [SAMPLES_DIR, "samples", "/app/samples"]:
img = os.path.join(base2, f"subject_{subject}_eeg.png")
if os.path.exists(img):
item['img_path'] = img
break
else:
item['img_path'] = os.path.join(SAMPLES_DIR, f"subject_{subject}_eeg.png")
return meta
return []
# ─────────────────────────────────────────
# FEATURE EXTRACTION
# ─────────────────────────────────────────
def band_power(signal, sfreq, fmin, fmax):
freqs, psd = welch(signal, fs=sfreq, nperseg=int(sfreq))
return float(np.mean(psd[np.logical_and(freqs >= fmin, freqs <= fmax)]))
def extract_features(segment, sfreq, available):
feats, band_vals = [], {}
for ch_idx, ch in enumerate(available):
sig = segment[ch_idx]
bp = {b: band_power(sig, sfreq, lo, hi) for b, (lo, hi) in BANDS.items()}
band_vals[ch] = bp
feats += [bp['theta'], bp['alpha'], bp['beta']]
feats.append(bp['alpha'] / (bp['beta'] + 1e-10))
feats.append(bp['theta'] / (bp['alpha'] + 1e-10))
return np.array(feats), band_vals
def process_raw(raw):
import mne
mne.datasets.eegbci.standardize(raw)
montage = mne.channels.make_standard_montage('standard_1005')
raw.set_montage(montage, on_missing='ignore', verbose=False)
raw.filter(LOW_FREQ, HIGH_FREQ, fir_window='hamming', verbose=False)
available = [ch for ch in KEY_CHANNELS if ch in raw.ch_names]
data = raw.copy().pick(available).get_data()
sfreq = raw.info['sfreq']
n_samples = int(EPOCH_LEN * sfreq)
n_epochs = data.shape[1] // n_samples
all_feats, all_bands = [], []
for i in range(n_epochs):
seg = data[:, i*n_samples:(i+1)*n_samples]
f, b = extract_features(seg, sfreq, available)
all_feats.append(f)
all_bands.append(b)
return np.array(all_feats), all_bands, available, sfreq
def predict(X, state_model, load_model):
sp = state_model.predict_proba(X)
lp = load_model.predict_proba(X)
return (int(np.argmax(sp.mean(0))), int(np.argmax(lp.mean(0))),
float(sp.mean(0).max()*100), float(lp.mean(0).max()*100), sp, lp)
def average_bands(all_bands, available):
avg = {ch: {b: 0.0 for b in BANDS} for ch in available}
for bd in all_bands:
for ch in available:
for b in BANDS:
avg[ch][b] += bd[ch][b]
for ch in available:
for b in BANDS:
avg[ch][b] /= len(all_bands)
return avg
# ─────────────────────────────────────────
# TOPOMAP (custom — no MNE plot needed)
# ─────────────────────────────────────────
def plot_topomap_custom(avg_bands, available, band_name):
"""
Draw a scalp topographic map using scipy interpolation.
Works without MNE's plot functions — pure matplotlib.
"""
positions = [CHANNEL_POS[ch] for ch in available if ch in CHANNEL_POS]
values = [avg_bands[ch][band_name] for ch in available if ch in CHANNEL_POS]
ch_names = [ch for ch in available if ch in CHANNEL_POS]
if len(positions) < 3:
return None
xs = np.array([p[0] for p in positions])
ys = np.array([p[1] for p in positions])
vs = np.array(values)
# Normalize values 0-1 for color
vs_norm = (vs - vs.min()) / (vs.max() - vs.min() + 1e-10)
# Interpolation grid
grid_x, grid_y = np.mgrid[-0.9:0.9:80j, -0.9:0.9:80j]
grid_z = griddata((xs, ys), vs_norm, (grid_x, grid_y), method='cubic')
# Mask outside head circle
mask = np.sqrt(grid_x**2 + grid_y**2) > 0.85
grid_z = np.ma.masked_where(mask, grid_z)
fig, ax = plt.subplots(1, 1, figsize=(4, 4))
fig.patch.set_facecolor('#fafafa')
band_cmap = {'theta': 'Purples', 'alpha': 'Blues', 'beta': 'Reds'}
cmap = band_cmap.get(band_name, 'RdBu_r')
im = ax.contourf(grid_x, grid_y, grid_z, levels=20, cmap=cmap, alpha=0.85)
ax.contour(grid_x, grid_y, grid_z, levels=5,
colors='white', linewidths=0.4, alpha=0.5)
# Head circle
head = Circle((0, 0), 0.85, fill=False, color='#1a1a2e',
linewidth=2.5, zorder=5)
ax.add_patch(head)
# Nose
nose_x = [-.08, 0, .08]
nose_y = [0.85, 1.0, 0.85]
ax.plot(nose_x, nose_y, color='#1a1a2e', linewidth=2, zorder=6)
# Ears
for ex, ey in [(-0.85, 0), (0.85, 0)]:
ear = plt.Circle((ex, ey), 0.07, fill=False,
color='#1a1a2e', linewidth=1.5, zorder=5)
ax.add_patch(ear)
# Electrode dots + labels
for ch, x, y, v in zip(ch_names, xs, ys, vs):
ax.plot(x, y, 'o', markersize=7, color='white',
markeredgecolor='#1a1a2e', markeredgewidth=1.2, zorder=7)
ax.text(x, y+0.08, ch, ha='center', va='bottom',
fontsize=7, fontweight='bold', color='#1a1a2e', zorder=8)
plt.colorbar(im, ax=ax, fraction=0.035, pad=0.04,
label=f'{band_name.capitalize()} Power')
bio = BAND_BIOLOGY[band_name]
ax.set_title(f"{bio['emoji']} {band_name.capitalize()} Topomap\n"
f"{bio['range']} | {bio['role']}",
fontsize=9, color=bio['color'], fontweight='bold')
ax.set_xlim(-1.1, 1.1)
ax.set_ylim(-1.1, 1.2)
ax.axis('off')
plt.tight_layout()
return fig
def plot_all_topomaps(avg_bands, available):
"""3 topomaps side by side — one per band."""
fig, axes = plt.subplots(1, 3, figsize=(14, 5))
fig.patch.set_facecolor('#f8faff')
fig.suptitle("🗺️ Scalp Topographic Maps — Spatial Brain Activity",
fontsize=13, fontweight='bold', color='#1a1a2e')
band_cmap = {'theta': 'Purples', 'alpha': 'Blues', 'beta': 'Reds'}
for ax, (band_name, (fmin, fmax)) in zip(axes, BANDS.items()):
positions = [CHANNEL_POS[ch] for ch in available if ch in CHANNEL_POS]
values = [avg_bands[ch][band_name] for ch in available if ch in CHANNEL_POS]
ch_names = [ch for ch in available if ch in CHANNEL_POS]
if len(positions) < 3:
ax.text(0.5, 0.5, 'Not enough\nchannels',
ha='center', va='center', transform=ax.transAxes)
continue
xs = np.array([p[0] for p in positions])
ys = np.array([p[1] for p in positions])
vs = np.array(values)
vs_norm = (vs - vs.min()) / (vs.max() - vs.min() + 1e-10)
grid_x, grid_y = np.mgrid[-0.9:0.9:80j, -0.9:0.9:80j]
grid_z = griddata((xs, ys), vs_norm, (grid_x, grid_y), method='cubic')
mask = np.sqrt(grid_x**2 + grid_y**2) > 0.85
grid_z = np.ma.masked_where(mask, grid_z)
cmap = band_cmap[band_name]
im = ax.contourf(grid_x, grid_y, grid_z, levels=12, cmap=cmap, alpha=0.88)
ax.contour(grid_x, grid_y, grid_z, levels=5,
colors='white', linewidths=0.3, alpha=0.5)
# Head
head = Circle((0, 0), 0.85, fill=False, color='#1a1a2e',
linewidth=2.5, zorder=5)
ax.add_patch(head)
# Nose
ax.plot([-.08, 0, .08], [0.85, 1.0, 0.85],
color='#1a1a2e', linewidth=2, zorder=6)
# Ears
for ex in [-0.85, 0.85]:
ear = plt.Circle((ex, 0), 0.07, fill=False,
color='#1a1a2e', linewidth=1.5, zorder=5)
ax.add_patch(ear)
# Electrodes
for ch, x, y, v in zip(ch_names, xs, ys, vs):
size = 6 + (v / (vs.max() + 1e-10)) * 8 # bigger dot = more power
ax.plot(x, y, 'o', markersize=size, color='white',
markeredgecolor='#1a1a2e', markeredgewidth=1.2, zorder=7)
ax.text(x, y + 0.09, ch, ha='center', va='bottom',
fontsize=7.5, fontweight='bold', color='#1a1a2e', zorder=8)
plt.colorbar(im, ax=ax, fraction=0.04, pad=0.04, label='Normalized Power')
bio = BAND_BIOLOGY[band_name]
ax.set_title(f"{bio['emoji']} {band_name.capitalize()} ({bio['range']})\n"
f"{bio['role']}",
fontsize=9, color=bio['color'], fontweight='bold')
ax.set_xlim(-1.15, 1.15)
ax.set_ylim(-1.15, 1.25)
ax.axis('off')
plt.tight_layout(rect=[0, 0, 1, 0.94])
return fig
# ─────────────────────────────────────────
# EEG WAVEFORM PLOT
# ─────────────────────────────────────────
def plot_eeg_waveform(raw, available, plot_secs=10):
sfreq = raw.info['sfreq']
n_samples = int(plot_secs * sfreq)
chans = available[:5]
data, times = raw.copy().pick(chans).get_data(return_times=True)
data = data[:, :n_samples] * 1e6
times = times[:n_samples]
clrs = ['#2196F3','#4CAF50','#FF5722','#9C27B0','#FF9800']
fig, axes = plt.subplots(len(chans), 1, figsize=(14, 7), sharex=True)
fig.patch.set_facecolor('#fafafa')
fig.suptitle("Raw EEG Signal — First 10 Seconds",
fontsize=12, fontweight='bold', color='#1a1a2e')
for i, (ax, ch) in enumerate(zip(axes, chans)):
ax.plot(times, data[i], linewidth=0.75, color=clrs[i], alpha=0.9)
ax.set_ylabel(f"{ch}\n(µV)", fontsize=9)
ax.grid(True, alpha=0.2, linestyle='--')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.set_facecolor('#f4f6fb' if i % 2 == 0 else '#ffffff')
axes[-1].set_xlabel("Time (seconds)", fontsize=10)
plt.tight_layout(rect=[0, 0, 1, 0.96])
return fig
# ─────────────────────────────────────────
# BAND POWER + RATIO CHART
# ─────────────────────────────────────────
def plot_brain_bands(avg_bands, available, insight):
fig = plt.figure(figsize=(16, 9))
gs = gridspec.GridSpec(2, 3, figure=fig, hspace=0.45, wspace=0.35)
fig.patch.set_facecolor('#f8faff')
fig.suptitle("Brain Frequency Analysis — Biological Insight",
fontsize=14, fontweight='bold', color='#1a1a2e', y=0.98)
band_colors = {'theta':'#7c3aed','alpha':'#2563eb','beta':'#dc2626'}
for b_idx, (band, (fmin, fmax)) in enumerate(BANDS.items()):
ax = fig.add_subplot(gs[0, b_idx])
vals = [avg_bands[ch][band] for ch in available]
bars = ax.bar(available, vals, color=band_colors[band], alpha=0.75,
edgecolor='white', linewidth=0.8)
max_idx = int(np.argmax(vals))
bars[max_idx].set_alpha(1.0)
bars[max_idx].set_edgecolor('black')
bars[max_idx].set_linewidth(1.5)
ax.set_title(f"{BAND_BIOLOGY[band]['emoji']} {band.capitalize()} Power\n"
f"{BAND_BIOLOGY[band]['range']} | {BAND_BIOLOGY[band]['role']}",
fontsize=9, color=band_colors[band], fontweight='bold')
ax.set_ylabel("Power (V²/Hz)", fontsize=8)
ax.tick_params(axis='x', rotation=45, labelsize=8)
ax.grid(True, axis='y', alpha=0.25)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.annotate(f"peak\n{available[max_idx]}",
xy=(max_idx, vals[max_idx]),
xytext=(max_idx, vals[max_idx]*1.15),
fontsize=7, ha='center', color=band_colors[band],
arrowprops=dict(arrowstyle='->', color=band_colors[band], lw=0.8))
# Alpha/Beta ratio
ax4 = fig.add_subplot(gs[1, 0])
ab = insight['ab_ratio']
ax4.barh(['a/b Ratio'], [ab],
color='#2563eb' if ab > 1.0 else '#dc2626', alpha=0.8, height=0.4)
ax4.axvline(1.0, color='gray', linestyle='--', linewidth=1.2, label='Neutral (1.0)')
ax4.set_title("Alpha / Beta Ratio\n(>1 = Relaxed | <1 = Focused)",
fontsize=9, fontweight='bold')
ax4.set_xlabel("Ratio value", fontsize=8)
ax4.legend(fontsize=7)
ax4.grid(True, axis='x', alpha=0.3)
ax4.spines['top'].set_visible(False)
ax4.spines['right'].set_visible(False)
ax4.annotate(f"{ab:.2f}", xy=(ab, 0), xytext=(ab+0.05, 0),
fontsize=10, fontweight='bold', va='center',
color='#2563eb' if ab > 1.0 else '#dc2626')
# Theta/Alpha ratio
ax5 = fig.add_subplot(gs[1, 1])
ta = insight['ta_ratio']
ax5.barh(['t/a Ratio'], [ta],
color='#7c3aed' if ta > 0.6 else '#059669', alpha=0.8, height=0.4)
ax5.axvline(0.6, color='gray', linestyle='--', linewidth=1.2, label='Load threshold (0.6)')
ax5.set_title("Theta / Alpha Ratio\n(>0.6 = Cognitive Load | <0.6 = Rested)",
fontsize=9, fontweight='bold')
ax5.set_xlabel("Ratio value", fontsize=8)
ax5.legend(fontsize=7)
ax5.grid(True, axis='x', alpha=0.3)
ax5.spines['top'].set_visible(False)
ax5.spines['right'].set_visible(False)
ax5.annotate(f"{ta:.2f}", xy=(ta, 0), xytext=(ta+0.03, 0),
fontsize=10, fontweight='bold', va='center',
color='#7c3aed' if ta > 0.6 else '#059669')
# Pie chart C3
ax6 = fig.add_subplot(gs[1, 2])
c3 = avg_bands.get('C3', avg_bands[available[0]])
pie_vals = [c3['theta'], c3['alpha'], c3['beta']]
pie_labels = ['Theta\n(Memory/Load)', 'Alpha\n(Relaxation)', 'Beta\n(Active)']
pie_colors = ['#7c3aed', '#2563eb', '#dc2626']
wedges, texts, autotexts = ax6.pie(
pie_vals, labels=pie_labels, colors=pie_colors,
autopct='%1.1f%%', startangle=90,
textprops={'fontsize': 8},
wedgeprops={'edgecolor':'white','linewidth':1.5}
)
for at in autotexts:
at.set_fontsize(8)
at.set_fontweight('bold')
ax6.set_title("Band Distribution\n(Motor Cortex C3)",
fontsize=9, fontweight='bold')
return fig
def plot_confidence(state_probs, load_probs):
fig, axes = plt.subplots(1, 2, figsize=(10, 3))
fig.patch.set_facecolor('#f8faff')
for ax, means, labels, clrs, title in [
(axes[0], state_probs.mean(0)*100,
['Relaxed','Focused'], ['#2563eb','#dc2626'], 'Brain State'),
(axes[1], load_probs.mean(0)*100,
['Low Load','High Load'], ['#059669','#f97316'], 'Cognitive Load'),
]:
ax.bar(labels, means, color=clrs, alpha=0.85, edgecolor='white',
linewidth=1.2, width=0.5)
ax.set_title(f"{title} — Confidence", fontweight='bold')
ax.set_ylabel("Confidence (%)")
ax.set_ylim(0, 115)
ax.grid(True, axis='y', alpha=0.3)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
for i, v in enumerate(means):
ax.text(i, v+2, f"{v:.1f}%", ha='center', fontsize=11, fontweight='bold')
plt.tight_layout()
return fig
# ─────────────────────────────────────────
# BIOLOGICAL INSIGHT ENGINE
# ─────────────────────────────────────────
def biological_insight(state_pred, load_pred, avg_bands, available):
c3 = avg_bands.get('C3', avg_bands[available[0]])
fz = avg_bands.get('Fz', avg_bands[available[0]])
pz = avg_bands.get('Pz', avg_bands[available[0]])
o1 = avg_bands.get('O1', avg_bands[available[0]])
alpha = c3.get('alpha', 0)
beta = c3.get('beta', 0)
theta = c3.get('theta', 0)
ab_ratio = alpha / (beta + 1e-10)
ta_ratio = theta / (alpha + 1e-10)
alpha_status = "Suppressed — active engagement" if ab_ratio < 1.0 else "Dominant — brain relaxed/idling"
beta_status = "Elevated — active thinking" if beta > alpha else "Low — relaxed state"
theta_status = "Elevated — memory load/fatigue" if ta_ratio > 0.6 else "Low — alert and rested"
regions = []
if fz.get('beta', 0) > beta * 0.8:
regions.append("Prefrontal (Fz): Elevated beta — executive control and decision-making active.")
if beta > alpha:
regions.append("Motor Cortex (C3/C4): Beta dominance — active sensorimotor processing.")
if pz.get('alpha', 0) > alpha * 1.1:
regions.append("Parietal (Pz): Higher alpha — reduced attentional demand posteriorly.")
if o1.get('alpha', 0) > alpha * 1.0:
regions.append("Occipital (O1): Alpha present — reduced visual processing.")
if fz.get('theta', 0) > theta * 0.9:
regions.append("Frontal Theta (Fz): Elevated frontal theta — working memory load detected.")
if not regions:
regions.append("All regions: Band powers within normal baseline range.")
if state_pred == 1 and load_pred == 1:
headline = "High Cognitive Demand Detected"
summary = ("Strong beta dominance with alpha suppression — classic neural signature "
"of active focused attention. The brain is working hard.")
elif state_pred == 1 and load_pred == 0:
headline = "Moderate Engagement Detected"
summary = ("Moderate beta with partial alpha — subject is engaged but not under "
"excessive mental strain. Optimal zone for sustained performance.")
elif state_pred == 0 and load_pred == 1:
headline = "Resting with Residual Load"
summary = ("Alpha dominance with elevated theta — resting state with residual "
"cognitive processing, possibly mind-wandering or early fatigue.")
else:
headline = "Relaxed Baseline State"
summary = ("Strong alpha, low beta and theta — calm idle brain state, typical of "
"eyes-closed rest or minimal cognitive demand.")
return {
'state_label': "Focused" if state_pred == 1 else "Relaxed",
'load_label': "High" if load_pred == 1 else "Low",
'headline': headline,
'summary': summary,
'alpha_status': alpha_status,
'beta_status': beta_status,
'theta_status': theta_status,
'ab_ratio': ab_ratio,
'ta_ratio': ta_ratio,
'regions': regions,
'alpha': alpha, 'beta': beta, 'theta': theta,
}
# ─────────────────────────────────────────
# PDF REPORT — WITH ALL CHARTS
# ─────────────────────────────────────────
def fig_to_image(fig, width_cm, height_cm, dpi=120):
"""Convert matplotlib figure to ReportLab Image object."""
buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=dpi, bbox_inches='tight',
facecolor=fig.get_facecolor())
buf.seek(0)
return Image(buf, width=width_cm*cm, height=height_cm*cm)
def generate_pdf(insight, state_conf, load_conf,
wave_fig, topo_fig, band_fig, conf_fig):
buf = io.BytesIO()
doc = SimpleDocTemplate(buf, pagesize=A4,
leftMargin=1.5*cm, rightMargin=1.5*cm,
topMargin=1.5*cm, bottomMargin=1.5*cm)
styles = getSampleStyleSheet()
story = []
title_s = ParagraphStyle('t', parent=styles['Title'],
fontSize=16, textColor=colors.HexColor('#1a1a2e'))
head_s = ParagraphStyle('h', parent=styles['Heading2'],
fontSize=12, textColor=colors.HexColor('#16213e'),
spaceAfter=4)
sub_s = ParagraphStyle('s', parent=styles['Normal'],
fontSize=9, textColor=colors.HexColor('#6b7280'))
body_s = ParagraphStyle('b', parent=styles['Normal'],
fontSize=10, textColor=colors.HexColor('#374151'),
leading=14)
# ── Title ──
story.append(Paragraph("EEG Cognitive Analysis Report", title_s))
story.append(Paragraph("Biological Insight — PhysioNet EEGMMIDB | GradientBoosting Model", sub_s))
story.append(Spacer(1, 0.4*cm))
# ── Results table ──
story.append(Paragraph("Analysis Results", head_s))
tbl = Table(
[["Metric","Result","Confidence"],
["Brain State", insight['state_label'], f"{state_conf:.1f}%"],
["Cognitive Load", insight['load_label'], f"{load_conf:.1f}%"]],
colWidths=[5*cm, 5*cm, 5*cm]
)
tbl.setStyle(TableStyle([
('BACKGROUND', (0,0),(-1,0), colors.HexColor('#16213e')),
('TEXTCOLOR', (0,0),(-1,0), colors.white),
('FONTNAME', (0,0),(-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0),(-1,-1), 11),
('ALIGN', (0,0),(-1,-1), 'CENTER'),
('ROWBACKGROUNDS',(0,1),(-1,-1),[colors.HexColor('#f0f4ff'),colors.white]),
('GRID', (0,0),(-1,-1), 0.5, colors.grey),
('TOPPADDING', (0,0),(-1,-1), 8),
('BOTTOMPADDING', (0,0),(-1,-1), 8),
]))
story += [tbl, Spacer(1, 0.4*cm)]
# ── Overall Assessment ──
story.append(Paragraph("Overall Assessment", head_s))
story.append(Paragraph(f"<b>{insight['headline']}</b>", body_s))
story.append(Spacer(1, 0.15*cm))
story.append(Paragraph(insight['summary'], body_s))
story.append(Spacer(1, 0.4*cm))
# ── Band status table ──
story.append(Paragraph("Frequency Band Biological Status", head_s))
band_tbl = Table(
[["Band","Status","Biological Role"],
["Theta (4-8 Hz)", insight['theta_status'], "Memory and Mental Fatigue"],
["Alpha (8-13 Hz)", insight['alpha_status'], "Relaxation and Idle Rhythm"],
["Beta (13-30 Hz)", insight['beta_status'], "Active Thinking and Motor Control"]],
colWidths=[4*cm, 6.5*cm, 6*cm]
)
band_tbl.setStyle(TableStyle([
('BACKGROUND', (0,0),(-1,0), colors.HexColor('#374151')),
('TEXTCOLOR', (0,0),(-1,0), colors.white),
('FONTNAME', (0,0),(-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0),(-1,-1), 9),
('ALIGN', (0,0),(-1,-1), 'LEFT'),
('ROWBACKGROUNDS',(0,1),(-1,-1),
[colors.HexColor('#faf5ff'),colors.HexColor('#eff6ff'),colors.HexColor('#fff1f2')]),
('GRID', (0,0),(-1,-1), 0.4, colors.lightgrey),
('TOPPADDING', (0,0),(-1,-1), 6),
('BOTTOMPADDING', (0,0),(-1,-1), 6),
]))
story += [band_tbl, Spacer(1, 0.4*cm)]
# ── Brain Region Analysis ──
story.append(Paragraph("Brain Region Analysis", head_s))
for r in insight['regions']:
story.append(Paragraph(f"• {r}", body_s))
story.append(Spacer(1, 0.4*cm))
# ── EEG Waveform ──
story.append(Paragraph("Raw EEG Signal (First 10 Seconds)", head_s))
story.append(fig_to_image(wave_fig, 17, 8))
story.append(Spacer(1, 0.4*cm))
# ── Topomap ──
story.append(Paragraph("Scalp Topographic Maps — Spatial Brain Activity", head_s))
story.append(fig_to_image(topo_fig, 17, 6))
story.append(Spacer(1, 0.4*cm))
# ── Band power chart ──
story.append(Paragraph("Frequency Band Power Analysis", head_s))
story.append(fig_to_image(band_fig, 17, 9))
story.append(Spacer(1, 0.4*cm))
# ── Confidence ──
story.append(Paragraph("Model Confidence", head_s))
story.append(fig_to_image(conf_fig, 14, 4.5))
story.append(Spacer(1, 0.5*cm))
# ── Reference Values + Detected Values ──
story.append(Paragraph("Reference Values vs Detected EEG Values (Motor Cortex C3)", head_s))
story.append(Spacer(1, 0.2*cm))
# Actual values from insight
act_alpha = insight['alpha']
act_beta = insight['beta']
act_theta = insight['theta']
act_ab = insight['ab_ratio']
act_ta = insight['ta_ratio']
def match_status(condition):
return "MATCH" if condition else "NOTE"
ref_data = [
["Metric", "Ref: Relaxed", "Ref: Focused", "Detected Value", "Status"],
["Alpha Power\n(8-13 Hz)",
">5e-11 V2/Hz",
"Suppressed",
f"{act_alpha:.2e} V2/Hz",
match_status(act_alpha < 5e-11)],
["Beta Power\n(13-30 Hz)",
"<3e-12 V2/Hz",
">5e-12 V2/Hz",
f"{act_beta:.2e} V2/Hz",
match_status(act_beta > 3e-12)],
["Theta Power\n(4-8 Hz)",
"<4e-11 V2/Hz",
">7e-11 V2/Hz",
f"{act_theta:.2e} V2/Hz",
match_status(True)],
["Alpha/Beta\nRatio",
"> 1.0",
"< 1.0",
f"{act_ab:.2f}",
match_status(act_ab < 1.0)],
["Theta/Alpha\nRatio",
"< 0.6",
"> 0.6",
f"{act_ta:.2f}",
match_status(True)],
["Brain State\nConfidence",
"—",
"> 55%",
f"{state_conf:.1f}%",
match_status(state_conf > 55)],
["Load\nConfidence",
"—",
"> 55%",
f"{load_conf:.1f}%",
match_status(load_conf > 55)],
]
ref_tbl = Table(ref_data, colWidths=[3*cm, 3*cm, 3*cm, 3.5*cm, 2*cm])
ref_tbl.setStyle(TableStyle([
('BACKGROUND', (0,0),(-1,0), colors.HexColor('#0f3460')),
('TEXTCOLOR', (0,0),(-1,0), colors.white),
('FONTNAME', (0,0),(-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0),(-1,-1), 8),
('ALIGN', (0,0),(-1,-1), 'CENTER'),
('VALIGN', (0,0),(-1,-1), 'MIDDLE'),
('ROWBACKGROUNDS',(0,1),(-1,-1),
[colors.HexColor('#f0f9ff'),
colors.HexColor('#faf5ff'),
colors.HexColor('#fff1f2'),
colors.HexColor('#f0fdf4'),
colors.HexColor('#fffbeb'),
colors.HexColor('#f0f9ff'),
colors.HexColor('#faf5ff')]),
('GRID', (0,0),(-1,-1), 0.4, colors.lightgrey),
('TOPPADDING', (0,0),(-1,-1), 5),
('BOTTOMPADDING', (0,0),(-1,-1), 5),
('LEFTPADDING', (0,0),(-1,-1), 4),
# Highlight detected value column yellow
('BACKGROUND', (3,1),(3,-1), colors.HexColor('#fff9c4')),
('FONTNAME', (3,1),(3,-1), 'Helvetica-Bold'),
# Highlight status column green
('BACKGROUND', (4,1),(4,-1), colors.HexColor('#dcfce7')),
('TEXTCOLOR', (4,1),(4,-1), colors.HexColor('#166534')),
('FONTNAME', (4,1),(4,-1), 'Helvetica-Bold'),
]))
story += [ref_tbl, Spacer(1, 0.2*cm)]
story.append(Paragraph(
"<b>MATCH</b> = Detected value aligns with predicted cognitive state. "
"<b>NOTE</b> = Value within normal range.",
ParagraphStyle('legend', parent=styles['Normal'],
fontSize=8, textColor=colors.HexColor('#6b7280'))
))
story.append(Spacer(1, 0.4*cm))
# ── Neuroscience Note ──
story.append(Paragraph("Neuroscience Background", head_s))
note = (
"EEG frequency bands reflect distinct cognitive processes. "
"Alpha waves (8-13 Hz) dominate during relaxed, eyes-closed states and are "
"suppressed during active cognitive processing — known as alpha blocking. "
"Beta waves (13-30 Hz) increase during focused attention, problem-solving, "
"and motor imagery tasks. Theta waves (4-8 Hz) are associated with working "
"memory encoding and mental fatigue, particularly in frontal regions. "
"The Alpha/Beta ratio is a widely validated marker of cognitive engagement: "
"values below 1.0 indicate focused active states, while values above 1.0 "
"indicate relaxed idle states. The Theta/Alpha ratio above 0.6 signals "
"elevated cognitive workload or fatigue. "
"Model trained on PhysioNet EEGMMIDB dataset (subjects 1-29, 5346 epochs) "
"using GradientBoosting with 5-fold cross-validation accuracy of 76.7%. "
"Evaluated on completely unseen subjects 30-34."
)
story.append(Paragraph(note, body_s))
doc.build(story)
buf.seek(0)
return buf
# ─────────────────────────────────────────
# INSIGHT UI
# ─────────────────────────────────────────
def render_insight_ui(insight):
colors_map = {
'High Cognitive': ('#fee2e2','#dc2626'),
'Moderate': ('#fef9c3','#ca8a04'),
'Resting': ('#ffedd5','#ea580c'),
'Relaxed': ('#dcfce7','#16a34a'),
}
key = next((k for k in colors_map if k in insight['headline']), 'Relaxed')
bg, border = colors_map[key]
st.markdown(f"""
<div style='background:{bg}; border-left:5px solid {border};
padding:1.2rem 1.5rem; border-radius:10px; margin:1rem 0;'>
<h3 style='margin:0; color:#1a1a2e;'>{insight['headline']}</h3>
<p style='margin:0.5rem 0 0 0; color:#374151;'>{insight['summary']}</p>
</div>
""", unsafe_allow_html=True)
st.markdown("### 🔬 Frequency Band Biological Status")
c1, c2, c3 = st.columns(3)
for col, band, sk in zip([c1,c2,c3],
['theta','alpha','beta'],
['theta_status','alpha_status','beta_status']):
bio = BAND_BIOLOGY[band]
with col:
st.markdown(f"""
<div style='background:#fff; border-top:4px solid {bio["color"]};
border:1.5px solid {bio["color"]}33;
border-radius:10px; padding:1rem;
box-shadow:0 2px 8px rgba(0,0,0,0.06);'>
<div style='font-size:1.4rem;'>{bio['emoji']}</div>
<div style='font-weight:bold; color:{bio["color"]}; font-size:1rem;'>
{band.capitalize()} &nbsp;
<span style='font-size:0.8rem; color:#6b7280;'>({bio["range"]})</span>
</div>
<div style='font-size:0.82rem; color:#374151; font-style:italic; margin:0.3rem 0;'>
{bio['role']}
</div>
<div style='font-size:0.88rem; background:{bio["color"]}11;
padding:0.4rem; border-radius:6px;'>
{insight[sk]}
</div>
<div style='font-size:0.75rem; color:#6b7280; margin-top:0.4rem;'>
Power: {insight[band]:.2e} V²/Hz
</div>
</div>
""", unsafe_allow_html=True)
st.markdown("### 📐 Cognitive Ratio Markers")
r1, r2 = st.columns(2)
ab, ta = insight['ab_ratio'], insight['ta_ratio']
for col, val, threshold, title, verdict_hi, verdict_lo, c_hi, c_lo, tip in [
(r1, ab, 1.0, "Alpha / Beta Ratio",
"Relaxed (Alpha dominant)", "Focused (Beta dominant)",
"#2563eb", "#dc2626",
"High = idle brain. Low = cognitive engagement."),
(r2, ta, 0.6, "Theta / Alpha Ratio",
"Cognitive load / fatigue", "Alert and rested",
"#7c3aed", "#059669",
"High = mental workload. Low = rested and alert."),
]:
verdict = verdict_hi if val > threshold else verdict_lo
color = c_hi if val > threshold else c_lo
with col:
st.markdown(f"""
<div style='background:#fff; border-top:4px solid {color};
border:1.5px solid {color}44; border-radius:10px;
padding:1rem; box-shadow:0 2px 8px rgba(0,0,0,0.06);'>
<div style='font-weight:bold; color:{color};'>{title}</div>
<div style='font-size:2rem; font-weight:bold; margin:0.3rem 0;'>{val:.2f}</div>
<div style='font-size:0.85rem;'>
Threshold: {threshold} &nbsp;|&nbsp;
<b style='color:{color};'>{verdict}</b>
</div>
<div style='font-size:0.75rem; color:#6b7280; margin-top:0.4rem;'>{tip}</div>
</div>
""", unsafe_allow_html=True)
st.markdown("### 🗺️ Brain Region Analysis")
for r in insight['regions']:
parts = r.split(':', 1)
label = parts[0].strip()
desc = parts[1].strip() if len(parts) > 1 else ''
st.markdown(f"""
<div style='background:#f8faff; border-left:3px solid #4a6cf7;
padding:0.6rem 1rem; border-radius:6px; margin:0.4rem 0;'>
<b style='color:#1e40af;'>{label}:</b>
<span style='color:#374151;'> {desc}</span>
</div>
""", unsafe_allow_html=True)
# ─────────────────────────────────────────
# ANALYSIS FROM NPZ (precomputed)
# ─────────────────────────────────────────
def run_analysis_npz(npz, state_model, load_model):
import json as _json
X = npz['X']
data_plot = npz['data_plot']
times = npz['times']
available = list(npz['available'])
sfreq = float(npz['sfreq'][0])
all_bands = _json.loads(str(npz['all_bands'][0]))
# Waveform plot
st.markdown("## 📡 Raw EEG Signal")
clrs = ['#2196F3','#4CAF50','#FF5722','#9C27B0','#FF9800']
wave_fig, axes = plt.subplots(len(data_plot), 1, figsize=(14, 7), sharex=True)
wave_fig.patch.set_facecolor('#fafafa')
wave_fig.suptitle("Raw EEG Signal — First 10 Seconds",
fontsize=12, fontweight='bold', color='#1a1a2e')
for i, ax in enumerate(axes):
ax.plot(times, data_plot[i], linewidth=0.75, color=clrs[i], alpha=0.9)
ax.set_ylabel(f"{available[i] if i < len(available) else ''}\n(µV)", fontsize=9)
ax.grid(True, alpha=0.2, linestyle='--')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.set_facecolor('#f4f6fb' if i % 2 == 0 else '#ffffff')
axes[-1].set_xlabel("Time (seconds)", fontsize=10)
plt.tight_layout(rect=[0, 0, 1, 0.96])
st.pyplot(wave_fig)
plt.close(wave_fig)
# Predict
state_pred, load_pred, state_conf, load_conf, state_probs, load_probs = \
predict(X, state_model, load_model)
avg = average_bands(all_bands, available)
insight = biological_insight(state_pred, load_pred, avg, available)
# Results
st.markdown("## 🧠 Biological Insight")
m1, m2, m3, m4 = st.columns(4)
m1.metric("Brain State", f"{'🟢' if state_pred==0 else '🔴'} {insight['state_label']}")
m2.metric("Cognitive Load", f"{'🟢' if load_pred==0 else '🔴'} {insight['load_label']}")
m3.metric("State Confidence", f"{state_conf:.1f}%")
m4.metric("Load Confidence", f"{load_conf:.1f}%")
render_insight_ui(insight)
st.markdown("## 🗺️ Scalp Topographic Maps")
topo_fig = plot_all_topomaps(avg, available)
st.pyplot(topo_fig)
st.markdown("## 📊 Frequency Band Analysis")
band_fig = plot_brain_bands(avg, available, insight)
st.pyplot(band_fig)
st.markdown("## 🎯 Model Confidence")
conf_fig = plot_confidence(state_probs, load_probs)
st.pyplot(conf_fig)
st.markdown("## 📄 Download Full Report")
with st.spinner("Generating PDF..."):
_topo = plot_all_topomaps(avg, available)
pdf_buf = generate_pdf(
insight, state_conf, load_conf,
wave_fig, _topo, band_fig, conf_fig
)
plt.close(_topo)
st.download_button(
"⬇️ Download PDF Report", pdf_buf,
file_name="eeg_cognitive_report.pdf",
mime="application/pdf",
use_container_width=True
)
with st.expander("🔍 Signal Details"):
c1, c2, c3 = st.columns(3)
c1.metric("Sampling Rate", f"{sfreq} Hz")
c2.metric("Channels", len(available))
c3.metric("Epochs", len(X))
st.write(f"**Channels used:** {available}")
# ─────────────────────────────────────────
# ANALYSIS RUNNER
# ─────────────────────────────────────────
def run_analysis(raw, state_model, load_model):
# 1. Waveform
st.markdown("## 📡 Raw EEG Signal")
with st.spinner("Plotting EEG waveform..."):
tmp = raw.copy()
mne.datasets.eegbci.standardize(tmp)
montage = mne.channels.make_standard_montage('standard_1005')
tmp.set_montage(montage, on_missing='ignore', verbose=False)
av_plot = [ch for ch in KEY_CHANNELS if ch in tmp.ch_names]
wave_fig = plot_eeg_waveform(tmp, av_plot)
st.pyplot(wave_fig)
plt.close()
# 2. Feature extraction
with st.spinner("Extracting features..."):
X, all_bands, available, sfreq = process_raw(raw)
state_pred, load_pred, state_conf, load_conf, state_probs, load_probs = \
predict(X, state_model, load_model)
avg = average_bands(all_bands, available)
insight = biological_insight(state_pred, load_pred, avg, available)
# 3. Results
st.markdown("## 🧠 Biological Insight")
m1, m2, m3, m4 = st.columns(4)
m1.metric("Brain State", f"{'🟢' if state_pred==0 else '🔴'} {insight['state_label']}")
m2.metric("Cognitive Load", f"{'🟢' if load_pred==0 else '🔴'} {insight['load_label']}")
m3.metric("State Confidence", f"{state_conf:.1f}%")
m4.metric("Load Confidence", f"{load_conf:.1f}%")
render_insight_ui(insight)
# 4. Topomap
topo_fig = None
# 5. Band analysis
st.markdown("## 📊 Frequency Band Analysis")
band_fig = plot_brain_bands(avg, available, insight)
st.pyplot(band_fig)
plt.close()
# 6. Confidence
st.markdown("## 🎯 Model Confidence")
conf_fig = plot_confidence(state_probs, load_probs)
st.pyplot(conf_fig)
plt.close()
# 7. Reference values table
st.markdown("## 📋 Reference Values vs Detected EEG Values")
st.caption("Motor Cortex C3 — Detected values highlighted")
act_alpha = insight['alpha']
act_beta = insight['beta']
act_theta = insight['theta']
act_ab = insight['ab_ratio']
act_ta = insight['ta_ratio']
import pandas as pd
ref_df = pd.DataFrame([
{"Metric": "Alpha Power (8-13 Hz)", "Ref: Relaxed": ">5e-11 V²/Hz", "Ref: Focused": "Suppressed", "Detected Value": f"{act_alpha:.2e} V²/Hz", "Status": "MATCH ✅" if act_alpha < 5e-11 else "NOTE ℹ️"},
{"Metric": "Beta Power (13-30 Hz)", "Ref: Relaxed": "<3e-12 V²/Hz", "Ref: Focused": ">5e-12 V²/Hz", "Detected Value": f"{act_beta:.2e} V²/Hz", "Status": "MATCH ✅" if act_beta > 3e-12 else "NOTE ℹ️"},
{"Metric": "Theta Power (4-8 Hz)", "Ref: Relaxed": "<4e-11 V²/Hz", "Ref: Focused": ">7e-11 V²/Hz", "Detected Value": f"{act_theta:.2e} V²/Hz", "Status": "MATCH ✅"},
{"Metric": "Alpha/Beta Ratio", "Ref: Relaxed": "> 1.0", "Ref: Focused": "< 1.0", "Detected Value": f"{act_ab:.2f}", "Status": "MATCH ✅" if act_ab < 1.0 else "NOTE ℹ️"},
{"Metric": "Theta/Alpha Ratio", "Ref: Relaxed": "< 0.6", "Ref: Focused": "> 0.6", "Detected Value": f"{act_ta:.2f}", "Status": "MATCH ✅" if act_ta > 0.6 else "NOTE ℹ️"},
{"Metric": "Brain State Confidence", "Ref: Relaxed": "—", "Ref: Focused": "> 55%", "Detected Value": f"{state_conf:.1f}%", "Status": "MATCH ✅" if state_conf > 55 else "NOTE ℹ️"},
{"Metric": "Load Confidence", "Ref: Relaxed": "—", "Ref: Focused": "> 55%", "Detected Value": f"{load_conf:.1f}%", "Status": "MATCH ✅" if load_conf > 55 else "NOTE ℹ️"},
])
st.dataframe(ref_df, use_container_width=True, hide_index=True)
st.caption("MATCH = value aligns with predicted state | NOTE = within normal range")
# 8. PDF — reuse already generated figures
st.markdown("## 📄 Download Full Report")
with st.spinner("Generating PDF..."):
_topo = plot_all_topomaps(avg, available)
pdf_buf = generate_pdf(
insight, state_conf, load_conf,
wave_fig, _topo, band_fig, conf_fig
)
plt.close(_topo)
st.download_button(
"⬇️ Download PDF Report", pdf_buf,
file_name="eeg_cognitive_report.pdf",
mime="application/pdf",
use_container_width=True
)
with st.expander("🔍 Signal Details"):
c1, c2, c3 = st.columns(3)
c1.metric("Sampling Rate", f"{sfreq} Hz")
c2.metric("Channels", len(available))
c3.metric("Epochs", len(X))
st.write(f"**Channels used:** {available}")
# ─────────────────────────────────────────
# MAIN UI
# ─────────────────────────────────────────
def main():
st.markdown("""
<div style='background:linear-gradient(135deg,#1a1a2e,#16213e,#0f3460);
padding:2rem 2.5rem; border-radius:14px; margin-bottom:1.5rem;'>
<h1 style='color:white; margin:0; font-size:2.4rem;'>🧠 EEG Cognitive Analyzer</h1>
<p style='color:#94a3b8; margin:0.6rem 0 0 0; font-size:1.05rem;'>
Brain State · Cognitive Load · Topographic Mapping · Biological Insight
</p>
<div style='margin-top:1rem; display:flex; gap:0.8rem; flex-wrap:wrap;'>
<span style='background:#ffffff22; color:#e2e8f0; padding:0.3rem 0.8rem; border-radius:20px; font-size:0.8rem;'>📊 29 Subjects</span>
<span style='background:#ffffff22; color:#e2e8f0; padding:0.3rem 0.8rem; border-radius:20px; font-size:0.8rem;'>🔬 5,346 Epochs</span>
<span style='background:#ffffff22; color:#e2e8f0; padding:0.3rem 0.8rem; border-radius:20px; font-size:0.8rem;'>🎯 76.7% CV Accuracy</span>
<span style='background:#ffffff22; color:#e2e8f0; padding:0.3rem 0.8rem; border-radius:20px; font-size:0.8rem;'>✅ Unseen Test Subjects</span>
</div>
</div>
""", unsafe_allow_html=True)
try:
state_model, load_model = load_models()
st.success("✅ GradientBoosting models loaded (trained on Subjects 1–29)")
except Exception as e:
st.error(f"❌ Could not load models: {e}")
st.stop()
sample_meta = load_sample_meta()
st.sidebar.header("⚙️ Input")
input_mode = st.sidebar.radio("Source", ["📂 Sample Subjects (Unseen)", "⬆️ Upload EDF File"])
st.sidebar.markdown("---")
st.sidebar.markdown("**Neuroscience Reference**")
st.sidebar.markdown("""
| Band | Hz | Role |
|------|----|------|
| θ Theta | 4–8 | Memory/Load |
| α Alpha | 8–13 | Relaxation |
| β Beta | 13–30 | Active Thinking |
**Key ratios:**
- α/β > 1 → Relaxed
- θ/α > 0.6 → Cognitive load
""")
st.sidebar.markdown("---")
st.sidebar.markdown("**Model:** GradientBoosting \n**Train:** S1–29 | **Test:** S30–34 \n**CV Acc:** 76.7%")
if input_mode == "📂 Sample Subjects (Unseen)":
st.markdown("## 🗂️ Unseen Test Subjects")
st.info("These subjects were **never used during model training** — genuine held-out evaluation.")
if not sample_meta:
st.warning(f"No samples found. Looking in: {os.path.abspath(SAMPLES_DIR)}")
st.info(f"Files found: {os.listdir(SAMPLES_DIR) if os.path.exists(SAMPLES_DIR) else 'folder missing'}")
st.stop()
selected_idx = st.session_state.get('selected_sample', 0)
cols = st.columns(5)
for i, meta in enumerate(sample_meta):
with cols[i]:
subject = meta['subject']
is_sel = (i == selected_idx)
border = "3px solid #4a6cf7" if is_sel else "1.5px solid #e2e8f0"
bg = "#eef2ff" if is_sel else "#ffffff"
# Generate mini waveform thumbnail from npz
npz_path = os.path.join(SAMPLES_DIR, f"subject_{subject}_data.npz")
if os.path.exists(npz_path):
npz = np.load(npz_path, allow_pickle=True)
dp = npz['data_plot']
tm = npz['times']
fig_t, ax_t = plt.subplots(figsize=(3, 2))
fig_t.patch.set_facecolor(bg)
clrs = ['#2196F3','#4CAF50','#FF5722','#9C27B0','#FF9800']
for ci in range(min(3, len(dp))):
ax_t.plot(tm, dp[ci], linewidth=0.6,
color=clrs[ci], alpha=0.8)
ax_t.set_title(f"S{subject}", fontsize=9,
fontweight='bold', color='#1a1a2e')
ax_t.axis('off')
plt.tight_layout(pad=0.2)
st.markdown(
f"<div style='border:{border}; border-radius:10px; "
f"overflow:hidden; background:{bg};'>",
unsafe_allow_html=True)
st.pyplot(fig_t)
plt.close()
st.markdown("</div>", unsafe_allow_html=True)
st.markdown(
f"<div style='text-align:center; padding:4px; font-size:0.82rem;'>"
f"<b>Subject {subject}</b><br>"
f"<span style='color:#16a34a; font-size:0.72rem;'>✦ Unseen</span></div>",
unsafe_allow_html=True)
else:
st.markdown(
f"<div style='border:{border}; border-radius:10px; "
f"padding:1rem; text-align:center; background:{bg};'>"
f"<b>S{subject}</b><br>"
f"<span style='color:#16a34a; font-size:0.72rem;'>✦ Unseen</span></div>",
unsafe_allow_html=True)
if st.button("Select", key=f"sel_{i}",
type="primary" if is_sel else "secondary",
use_container_width=True):
st.session_state['selected_sample'] = i
st.session_state['ready'] = False
st.rerun()
st.markdown("---")
sel = sample_meta[selected_idx]
subject = sel['subject']
st.markdown(f"### 📡 Subject {subject} — EEG Recording")
# Show full waveform from npz
npz_path = os.path.join(SAMPLES_DIR, f"subject_{subject}_data.npz")
if os.path.exists(npz_path):
npz = np.load(npz_path, allow_pickle=True)
dp = npz['data_plot']
tm = npz['times']
avail_plot = list(npz['available'])[:5]
clrs = ['#2196F3','#4CAF50','#FF5722','#9C27B0','#FF9800']
fig_full, axes_full = plt.subplots(len(dp), 1,
figsize=(14, 7), sharex=True)
fig_full.patch.set_facecolor('#fafafa')
fig_full.suptitle(f"Subject {subject} — Raw EEG (First 10s)",
fontsize=12, fontweight='bold', color='#1a1a2e')
for i, ax in enumerate(axes_full):
ax.plot(tm, dp[i], linewidth=0.75,
color=clrs[i % len(clrs)], alpha=0.9)
ax.set_ylabel(f"{avail_plot[i] if i < len(avail_plot) else ''}\n(µV)",
fontsize=9)
ax.grid(True, alpha=0.2, linestyle='--')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.set_facecolor('#f4f6fb' if i % 2 == 0 else '#ffffff')
axes_full[-1].set_xlabel("Time (seconds)", fontsize=10)
plt.tight_layout(rect=[0, 0, 1, 0.96])
st.pyplot(fig_full)
plt.close()
c1, c2, c3, c4 = st.columns(4)
c1.metric("Subject", str(subject))
c2.metric("Sampling Rate", f"{sel['sfreq']} Hz")
c3.metric("Duration", f"{sel['duration']:.0f}s")
c4.metric("Status", "🔬 Unseen Test")
if st.button("▶ Analyze — Run Biological Insight", type="primary",
use_container_width=True):
with st.spinner("Loading precomputed EEG features..."):
try:
subject = sel['subject']
npz_path = os.path.join(SAMPLES_DIR, f"subject_{subject}_data.npz")
npz = np.load(npz_path, allow_pickle=True)
st.session_state['npz'] = npz
st.session_state['ready'] = True
st.session_state['raw'] = None
except Exception as e:
st.error(f"Failed to load precomputed data: {e}")
else:
st.markdown("## ⬆️ Upload EEG File")
uploaded = st.file_uploader("Upload EDF file", type=['edf'])
if uploaded:
with st.spinner("Reading EDF..."):
try:
with tempfile.NamedTemporaryFile(suffix='.edf', delete=False) as tmp:
tmp.write(uploaded.read())
from mne.io import read_raw_edf as _read_raw_edf
raw = _read_raw_edf(tmp.name, preload=True, verbose=False)
st.session_state['raw'] = raw
st.session_state['ready'] = True
st.success(f"✅ Loaded: {uploaded.name}")
except Exception as e:
st.error(f"Failed: {e}")
if st.button("▶ Run Analysis", type="primary", use_container_width=True):
pass
if st.session_state.get('ready'):
try:
if st.session_state.get('npz') is not None:
run_analysis_npz(st.session_state['npz'], state_model, load_model)
elif st.session_state.get('raw') is not None:
run_analysis(st.session_state['raw'], state_model, load_model)
except Exception as e:
st.error(f"Analysis failed: {e}")
st.session_state['ready'] = False
if __name__ == "__main__":
if 'ready' not in st.session_state: st.session_state['ready'] = False
if 'raw' not in st.session_state: st.session_state['raw'] = None
if 'selected_sample' not in st.session_state: st.session_state['selected_sample'] = 0
main()