π§ Real-time, non-invasive stress & drowsiness detection using only a standard webcam. Combines facial action unit analysis with remote photoplethysmography (rPPG) in a privacy-first, fully on-device pipeline.
| Capability | How |
|---|---|
| π Live Heart Rate | Remote PPG via webcam green channel β POS algorithm with automatic CHROM fallback, Kalman-filtered BPM |
| π HRV (RMSSD) | Inter-beat interval analysis with IBI outlier rejection (capped 150 ms) |
| ποΈ Drowsiness Detection | PERCLOS proxy β sustained low Eye Aspect Ratio (per-user adaptive threshold) > 2.5 s β amber flashing banner + 3Γ alarm beep |
| π§© 11-D Multimodal Fusion | 9 behavioral (EAR, brow, head pose, blinks, lip depression, jaw clenching) + 2 physiological (BPM, HRV) β RF + ExtraTrees ensemble |
| π― Continuous Stress Score | Rule-based 0β100 score (BPM + HRV + AU4 brow + EAR) β always live, independent of signal quality |
| π State Recovery | Label-buffer vote-decay + score safety valve prevents stuck "Stressed" label |
| π Zero Cloud | All inference on-device. No API keys. No data leaves your machine. |
| π Webcam Reconnect | Automatic reconnect with exponential failure cutoff (10 retries) |
| π HTML Reports | Interactive post-session dashboard generated from CSV logs |
Run
python main.py --no-calibfor a quick test without the 15-second calibration phase.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β main.py (v3.1) β
β β
β Webcam βββΊ Worker Thread (daemon) β
β β β
β βββββββ΄ββββββββββββββββββββββ β
β βΌ βΌ β
β AUExtractor RPPGExtractor β
β (MediaPipe 468-LM) (rPPG β Green ch.) β
β β’ EAR / Blink β’ POS + CHROM fallback β
β β’ Brow furrow (AU4) β’ Kalman-filtered BPM β
β β’ Head pose β’ 45β150 BPM physiological gate β
β β’ Lip depression, jaw β’ IBI peaks β HRV β
β β’ PERCLOS drowsiness β’ SNR quality gate (tightened) β
β β β β
β βββββββββββββ¬ββββββββββββββββ β
β βΌ β
β StressClassifier β
β 11-D feature vector β
β RF + ExtraTrees VotingClassifier (soft) β
β + CalibratedClassifierCV (isotonic) β
β + Rule-based score 0β100 β
β β β
β ββββββββββββΌβββββββββββ β
β βΌ βΌ βΌ β
β HUD (cv2) Session HTML Report β
β CSV log (post-session) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Python 3.10+
- Webcam (built-in or USB)
- Windows (for audio alerts via
winsound; Linux/Mac require minor modification)
git clone https://github.com/SheeshDarth/Stress-Detection-System.git
cd Stress-Detection-System
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # Linux / macOS
pip install -r requirements.txtpython -c "
import urllib.request, os
os.makedirs('models', exist_ok=True)
urllib.request.urlretrieve(
'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task',
'models/face_landmarker.task'
)
print('Downloaded models/face_landmarker.task')
"# Full run with 15-second calibration phase (recommended)
python main.py
# Skip calibration (quick test)
python main.py --no-calib
# Use a specific camera index
python main.py --camera 1The classifier auto-trains on synthetic data on first run. To retrain manually, or to
re-extract features from the UBFC-Phys dataset (download_dataset.py fetches it via
kagglehub):
python train.py # retrain on synthetic + any cached UBFC features
python train.py --extract # re-extract UBFC-Phys features first| Key | Action |
|---|---|
| ESC / Q | Quit |
| R | Reset all signal buffers |
| M | Toggle face mesh overlay |
| S | Save screenshot |
| L | Start / stop CSV session logging |
| A | Toggle audio alerts |
# Full test suite (68 tests)
pytest tests/ -v
# Unit tests only (fast, no webcam needed)
pytest tests/test_classifier.py tests/test_rppg.py tests/test_au_extraction.py -v
# Integration test (generates synthetic video, runs full pipeline headlessly)
pytest tests/test_integration.py -v -m integration
# Headless sanity check (no webcam required)
python sanity_check.py
# Ablation study, baselines, significance tests, temporal-stability diagnostic
python run_ablation.py --save results/ablation_results.csvStress-Detection-System/
βββ main.py # Real-time webcam application (v3.1)
βββ train.py # Training pipeline
βββ run_ablation.py # Ablation study, baselines, significance tests
βββ sanity_check.py # Headless pipeline verification
βββ download_dataset.py # UBFC-Phys / WESAD dataset downloader
βββ requirements.txt
βββ README.md
βββ context.md # Full technical changelog (v3.0 -> v3.1)
β
βββ models/
β βββ face_landmarker.task # MediaPipe model (download separately)
β βββ stress_model.pkl # Trained classifier (auto-generated)
β βββ stress_model_metrics.json
β
βββ results/
β βββ ablation_results_summary.json # Ablation + baseline + significance results
β
βββ src/
β βββ data_loader.py # UBFC-Phys dataset loader
β βββ session_logger.py # CSV session logging with finalise
β βββ report_generator.py # HTML post-session report generator
β βββ visual/
β β βββ au_extraction.py # MediaPipe AU extraction (+ lip/jaw, v3.1)
β βββ physiological/
β β βββ rppg_extraction.py # rPPG pipeline (+ CHROM, Kalman, v3.1)
β βββ fusion/
β βββ classifier.py # 11-D multimodal fusion classifier (v3.1)
β
βββ tests/
β βββ test_au_extraction.py
β βββ test_rppg.py
β βββ test_classifier.py
β βββ test_session_logger.py
β βββ test_integration.py # Synthetic video integration test
β
βββ logs/ # Auto-created; session CSVs + app log
Full 11-D system (RF + ExtraTrees soft-voting ensemble, CalibratedClassifierCV),
5-fold cross-validation on 2,300 samples:
| Metric | Value |
|---|---|
| Accuracy | 98.52% Β± 0.46% |
| F1 Score | 98.53% Β± 0.45% |
| ROC AUC | 99.95% Β± 0.04% |
run_ablation.py runs an ablation + baseline + statistical-significance comparison
against the full 11-D system (results in results/ablation_results_summary.json).
Every row is 5-fold cross-validated on the same 2,300-sample dataset as the full system.
Ablation β does each component earn its place?
| Configuration | Dim | Accuracy | F1 | vs. full system |
|---|---|---|---|---|
| A1 β Full system (11D, RF+ET) | 11 | 98.52% | 98.53% | β |
| A2 β No lip/jaw (9D, RF+ET) | 9 | 97.39% | 97.43% | full system better, p = 0.033 |
| A3 β Behavioral only (9D, RF+ET) | 9 | 98.39% | 98.40% | no significant difference, p = 0.071 |
| A4 β Physiological only (2D, RF+ET) | 2 | 86.43% | 86.30% | full system better, p = 0.0003 |
| A5 β RF only, no ExtraTrees (11D) | 11 | 98.26% | 98.28% | full system better, p = 0.032 |
Baselines β how do other classifiers do on the same 11-D features?
| Configuration | Accuracy | F1 | vs. full system |
|---|---|---|---|
| B1 β SVM-RBF | 99.17% | 99.17% | no significant difference, p = 0.157 |
| B2 β Logistic Regression | 98.74% | 98.74% | no significant difference, p = 0.514 |
| B3 β k-NN (k=5) | 98.26% | 98.23% | no significant difference, p = 0.444 |
| B4 β AU-only RF (7D behavioral) | 96.61% | 96.66% | full system better, p = 0.014 |
| B5 β BPM threshold (1D) | 78.22% | 78.89% | full system better, p < 0.0001 |
Reading it honestly: the lip/jaw features and multi-classifier ensembling each contribute a statistically significant lift over an ablated version, and dropping to physiological-only or a single naive threshold costs real accuracy. Several strong baselines (SVM-RBF, logistic regression, k-NN) land within noise of the full system β RF + ExtraTrees was kept because it ties the strongest baselines while providing calibrated probabilities and interpretable Gini feature importances, not because it's the single best number in the table.
Temporal stability β does the hysteresis + override logic matter?
| Configuration | Label flip rate | Accuracy |
|---|---|---|
| With hysteresis + score override | 0.0 | 100% |
| With hysteresis, no override | 0.0 | 100% |
| No hysteresis, no override | 5.0 | 95.8% |
In the synthetic transition-stability diagnostic, removing the 40-vote hysteresis
buffer and rule-based override reintroduces label flapping and measurably drops
accuracy β the reason both exist in main.py rather than a raw per-frame prediction.
- MediaPipe FaceLandmarker β 468 3-D landmarks at 25+ FPS (CPU-only)
- Eye Aspect Ratio (EAR) β adaptive blink threshold (calibrated from first 90 frames), reused for per-user drowsiness sensitivity
- Brow furrow (AU4) β normalized inner-eyebrow distance
- Head pose β nose-tip displacement variance and mean movement
- Lip depression & jaw clenching β AU15 proxy and chin-to-nose distance variance (new in v3.1)
- PERCLOS β consecutive frames below the adaptive EAR threshold β drowsiness flag at 2.5 s
- 10-second windowed aggregation β 9 statistical features
- ROI extraction β forehead + cheek regions via convex hull of landmarks
- Spatial RGB mean β per-frame in a 10-second rolling buffer
- POS algorithm β Plane-Orthogonal-to-Skin projection, with automatic CHROM fallback when POS SNR < 1.0
- Butterworth bandpass β widened to 0.75β2.5 Hz (45β150 BPM), covering athletic and elevated-HR users
- Welch PSD + Kalman filter β dominant frequency β BPM, smoothed by a 1-D Kalman filter (replaces the earlier rolling median for lower lag)
- IBI outlier rejection β Β±25% of median, RMSSD capped at 150 ms
- SNR quality gate β "Poor / Fair / Good" (tightened thresholds: Good > 5.0, Fair > 1.0)
| Mode | When Active | Basis |
|---|---|---|
| Rule-based score (0β100) | Always | Physiological thresholds on BPM, HRV, brow, EAR |
| ML binary label | Signal β₯ "Fair" + buffer β₯ 35% | RF + ExtraTrees ensemble with 75% hysteresis over 40-vote window |
| Score override | Score < 28 | Snaps to "Normal", clears stale ML votes immediately |
See context.md for the full file-by-file v3.0 β v3.1 changelog.
- 100% on-device β no video, biometric data, or stress readings ever transmitted
- Session logs stored locally in
logs/(auto-created, owner-accessible) - Model loaded from local
.pklfile β no network calls at inference time - One-time internet access: MediaPipe model download and optional UBFC/WESAD dataset download
- Wang, W. et al. (2017). "Algorithmic principles of remote-PPG." IEEE TBME, 64(7), 1479β1491
- SoukupovΓ‘, T. & Δech, J. (2016). "Real-time eye blink detection using facial landmarks." CVWW
- de Haan, G. & Jeanne, V. (2013). "Robust pulse rate from chrominance-based rPPG." IEEE TBME
- Bobbia, S. et al. (2019). "Unsupervised skin tissue segmentation for rPPG." Pattern Recognition Letters
- UBFC-Phys Dataset β Meziati Sabour et al. (2021)
MIT License β see LICENSE file.
Built with β€οΈ for IPCV Project β all inference on-device, zero cloud dependencies.