Skip to content

Repository files navigation

StressShield

Real-time physiological stress detection from ECG and respiration signals, with a live web console and per-user calibration.

StressShield reads a BITalino sensor stream over LSL (Lab Streaming Layer), extracts HRV and respiration features every 5 seconds over a rolling 60-second window, and classifies the current state as stress or non-stress using a model trained on the WESAD dataset.

StressShield console

Features

  • Live classification — rolling 60 s window, prediction every 5 s
  • Web console — stress-probability trend, live ECG and respiration traces, and derived metrics (HR, RMSSD, LF/HF, breath rate, breath amplitude), streamed to the browser over Server-Sent Events
  • Per-user calibration — records a relaxed baseline while the user watches a calming video, then sets a personal threshold at baseline_mean + 1.5 × std (capped at 0.95). Profiles are stored in calibration.json and can be switched or deleted from the UI.
  • CSV export — record 1–300 s of raw signal and per-window predictions (including all extracted features) and download them as CSV
  • Light/dark theme

Hardware and setup

Requirement Detail
Sensor BITalino with ECG and respiration (PZT) channels
Software OpenSignals (r)evolution, streaming over LSL
Sampling rate 1000 Hz
Channels index 1 = respiration, index 2 = ECG
LSL stream name OpenSignals

These are set as constants at the top of app.py — change CH_ECG, CH_RESP, FS, or STREAM_NAME if your acquisition setup differs.

Installation

git clone https://github.com/sharduldhande/StressShield.git
cd StressShield
pip install -r requirements.txt

Key dependencies: flask, pylsl, neurokit2, scikit-learn, joblib, numpy.

Usage

  1. Connect the BITalino, open OpenSignals, and enable LSL streaming.
  2. Verify the stream is visible:
   python lsl_probe.py
  1. Start the console:
   python app.py
  1. Open http://localhost:5000.

The first 60 seconds fill the buffer (progress is shown in the UI) before the first prediction appears.

For a terminal-only version without the web UI, run python realtime_stress.py.

Calibrating a profile

  1. Click + Calibrate, enter a name, and click Start.
  2. Watch the calibration video and stay relaxed. The last 24 prediction windows (2 minutes) are used as the baseline.
  3. The computed threshold is saved and the profile becomes selectable from the dropdown.

Without a calibration profile the threshold defaults to 0.5.

The calibration video (4minbeach.mp4, referenced as CALIB_VIDEO_FILE) is not included in the repository — supply your own and place it in the project root.

Features extracted

ECG (via neurokit2.hrv) — mean NN, SDNN, RMSSD, pNN50, LF power, HF power, LF/HF ratio

Respiration (via neurokit2.rsp_process) — mean, SD, min and max breathing rate, mean amplitude

Windows where feature extraction fails (poor signal quality) are reported as errors rather than classified.

Model

Trained on the WESAD dataset; extracted features are in wesad_features.csv.

  • stress_model.pkl — full model including EDA features
  • stress_model_no_eda.pkl — ECG + respiration only; this is the one the app loads, since the runtime setup has no EDA channel

Each pickle holds a dict with model, scaler, and features keys. Retrain the EDA-free model with python retrain_no_eda.py.

Notebooks

  • stressdetect.ipynb — feature engineering and model training
  • Explainer.ipynb — model interpretation and analysis

Results

Evaluation protocol

The model is evaluated with leave-one-subject-out (LOSO) cross-validation on the 15 WESAD subjects (S2–S17, excluding S12): train on 14 subjects, test on the held-out one, repeat 15 times. Physiological signals vary far more between people than within one person, so a random train/test split leaks subject identity and inflates accuracy. LOSO is the harder, honest number and is what a new user of the device would actually experience.

Setting Value
Dataset WESAD chest RespiBAN (ECG, respiration; EDA dropped for the deployed model)
Sampling rate 700 Hz
Window 60 s, 30 s step (50% overlap)
Label rule majority label in window, ≥ 80% purity; baseline → 0, stress (TSST) → 1
Windows 883 total (313 stress, 570 non-stress; 35.4% stress)
Features 12: 7 HRV (mean NN, SDNN, RMSSD, pNN50, LF, HF, LF/HF) + 5 respiration (rate mean/SD/min/max, amplitude mean)
Classifier XGBoost, 300 trees, depth 4, lr 0.05, scale_pos_weight for class imbalance

LOSO performance

Metric Value
Accuracy 0.89
ROC-AUC 0.972
Stress precision / recall / F1 0.84 / 0.88 / 0.85
Non-stress precision / recall / F1 0.93 / 0.91 / 0.92
Macro F1 0.89

Per-subject AUC (held-out subject):

S2 S3 S4 S5 S6 S7 S8 S9 S10 S11 S13 S14 S15 S16 S17
0.93 0.99 1.00 1.00 1.00 1.00 1.00 1.00 0.31 1.00 0.99 1.00 0.96 1.00 0.99

Fourteen of fifteen subjects score ≥ 0.93 AUC. S10 is a clear outlier (0.31): the model's stress signature is inverted for this subject, which is exactly the inter-subject variability problem LOSO is meant to expose. This is the motivation for the per-user calibration step in the app (see Calibrating a profile): rather than trusting a population threshold of 0.5, each user records a relaxed baseline and the decision threshold is set at baseline_mean + 1.5 × std.

Reproduce: python retrain_no_eda.py prints the full LOSO report for the ECG + respiration model. stressdetect.ipynb contains the original feature extraction and the EDA-inclusive run.

What drives the predictions (SHAP)

SHAP values were computed with TreeExplainer on the deployed (no-EDA) model (Explainer.ipynb). Feature importance, gain-based:

Rank Feature Importance Physiological reading
1 hrv_meanNN 0.287 Shorter mean RR interval (higher heart rate) → stress
2 resp_rate_std 0.126 More irregular breathing → stress
3 hrv_pnn50 0.112 Less beat-to-beat variability (lower vagal tone) → stress
4 resp_amplitude_mean 0.109 Shallower breaths → stress
5 resp_rate_min 0.072
6 resp_rate_max 0.069
7 hrv_rmssd 0.063 Lower RMSSD (parasympathetic withdrawal) → stress
8 resp_rate_mean 0.053
9–12 hrv_hf, hrv_lf_hf, hrv_sdnn, hrv_lf 0.02–0.03 Frequency-domain HRV contributes least

The top features are the ones a clinician would expect: heart rate, vagal-tone markers (pNN50, RMSSD), and breathing regularity. Frequency-domain HRV (LF, HF, LF/HF) is weak, likely because a 60 s window is too short for a stable spectral estimate. Cardiac and respiratory features together account for the whole model, so the deployed system does not depend on the EDA channel that BITalino setups often lack.

Closed-loop intervention study

STAR is closed-loop: when the calibrated stress probability crosses the user's threshold, the system fades in a passive ambient-audio intervention (Weightless by Marconi Union, chosen for its documented anxiolytic effect and lyric-free, low-distraction profile) and fades it out when the state returns to non-stress. The intervention was validated in a pilot within-subject AB/BA crossover experiment (GT Human-Systems Integration project, manuscript submitted to ACM 2026).

Participants 6, each completing a familiarisation trial (discarded), one control trial and one intervention trial
Design Two-period crossover, order counterbalanced (AB / BA) across participants to cancel practice, fatigue and carry-over effects
Stressor / task Time-constrained square-tracing game: connect four target dots into as many accurate squares as possible under increasing time pressure; scored on straightness, completeness and path deviation
Control (A) Task with live stress prediction but no intervention
Intervention (B) Same task; ambient soundtrack triggered by the model whenever a stress window is detected
Primary outcome Share of 60 s windows classified as stress, and mean stress probability
Secondary outcome Task score
Outcome Control Intervention Change
Detected stress windows 40.7% 32.2% −8.5 pp
Mean stress probability 0.470 0.376 −0.094
Mean task score 73.88 76.16 +2.28

Four of six participants scored higher with the intervention; the other two were on par with control. A Cohen's d power analysis on the pilot suggests 41–55 participants would be needed for statistical significance, so these results are exploratory.

A crossover design was chosen over a between-subjects comparison for the same reason LOSO was chosen for evaluation: physiological baselines differ enormously between people, and using each participant as their own control removes that variance.

API

Endpoint Method Purpose
/stream GET SSE stream of predictions, status and calibration events
/signals GET SSE stream of downsampled ECG/respiration samples
/api/profiles GET List calibration profiles
/api/profiles/<name> DELETE Delete a profile
/api/select POST Activate a profile
/api/calibrate POST Begin baseline collection
/api/calibrate/finish POST Compute and save the threshold
/api/export/start POST Begin a timed recording
/api/export/signals GET Download raw signal CSV
/api/export/predictions GET Download prediction + feature CSV

Limitations

  • Research and educational use only. Not a medical device, and not validated for clinical or diagnostic use.
  • Trained on WESAD, whose participants and lab-induced stressors may not generalise to your users or conditions.
  • ECG quality is sensitive to electrode placement and movement; motion artefacts will produce dropped windows or unreliable probabilities.
  • Single-user, single-stream: the server assumes one connected device.
  • One subject (S10) is misclassified almost entirely; per-user calibration mitigates but does not eliminate this.
  • The runtime pipeline uses BITalino at 1000 Hz while training used RespiBAN at 700 Hz; features are rate-normalised by neurokit2, but a domain gap remains.
  • Windows where neurokit2 fails to find R-peaks (poor contact, motion) are dropped rather than classified.

About

Real-time stress detection from ECG + respiration (XGBoost on WESAD, 89% LOSO) with per-user calibration and closed-loop intervention

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages