Skip to content

Commit 9ba1cf1

Browse files
committed
Refactor into installable package with FSM, MediaPipe backend, and tests
The previous version was a single ~150-line script that worked but had a few sharp edges in production use: - The alarm call was synchronous (playsound), so every drowsy trigger stalled the detection loop while audio played, and the alarm fired every frame eyes stayed closed (overlapping audio). - Landmark detection went through dlib/face_recognition, which requires CMake and is genuinely painful to install. - No state machine: a one-frame reopen mid-yawn reset the alarm countdown to zero. A briefly lost detection did the same. - No tests, no CI, no library-shaped API — it was a script. This rewrite turns the project into a small Python package while keeping the IEEE paper as the spiritual reference: - New 'drowsiness' package with a stateful DrowsinessDetector (AWAKE/DROWSY FSM with hysteresis), a DrowsinessConfig dataclass, and DrowsyEvent objects that fire exactly once per drowsy episode. on_event callback for downstream integrations. - MediaPipe FaceMesh is the new default backend — pure pip, significantly faster, no native build. The legacy face_recognition backend is still available behind --backend face_recognition / extras='legacy'. - Background, cooldown-debounced alarm thread; never blocks the detection loop, soft-fails when the audio file is missing, falls back through afplay/aplay/winsound. - 'drowsy' CLI with two subcommands: - 'drowsy run <source>' — live overlay window with FPS and color-coded eye polylines. - 'drowsy analyze <video> --output-dir report/' — headless batch that emits ear.csv, events.jsonl, summary.json. - 14 pytest cases covering EAR math, FSM transitions, debounce behavior, lost-face robustness, alarm cooldown, and config validation. Backend is mocked via a tiny FakeBackend so tests don't need a webcam. - pyproject.toml with audio/mediapipe/legacy extras, requirements, GitHub Actions matrix on Python 3.9/3.11/3.12, ruff-clean. - README rewritten with a comparison table, FSM diagram, batch report layout, and a candid limitations section.
1 parent dfb8aa1 commit 9ba1cf1

13 files changed

Lines changed: 1364 additions & 250 deletions

File tree

.github/workflows/tests.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
python-version: ["3.9", "3.11", "3.12"]
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Set up Python ${{ matrix.python-version }}
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: ${{ matrix.python-version }}
23+
cache: pip
24+
25+
- name: Install dependencies
26+
run: |
27+
python -m pip install --upgrade pip
28+
pip install numpy "opencv-python-headless>=4.5" pytest pytest-cov ruff
29+
30+
- name: Install package (no audio/landmark extras for tests)
31+
run: pip install -e .
32+
33+
- name: Lint
34+
run: ruff check drowsiness tests
35+
36+
- name: Run tests
37+
run: pytest --cov=drowsiness --cov-report=term-missing

README.md

Lines changed: 169 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,154 +1,229 @@
1-
# 🚗💤 Drowsiness Detection with OpenCV
1+
# 🚗💤 Drowsiness Detection
22

3-
> **Real-time driver drowsiness detection** using computer vision and the Eye Aspect Ratio (EAR) algorithm.
3+
[![Tests](https://github.com/Sanjays2402/Drowsiness-Detection-with-OpenCV/actions/workflows/tests.yml/badge.svg)](https://github.com/Sanjays2402/Drowsiness-Detection-with-OpenCV/actions/workflows/tests.yml)
4+
[![IEEE](https://img.shields.io/badge/IEEE-Published%20Paper-00629B?logo=ieee&logoColor=white)](https://ieeexplore.ieee.org/document/9532758)
5+
![Python](https://img.shields.io/badge/Python-3.9%2B-3776AB?logo=python&logoColor=white)
6+
![OpenCV](https://img.shields.io/badge/OpenCV-4.x-5C3EE8?logo=opencv&logoColor=white)
7+
![MediaPipe](https://img.shields.io/badge/MediaPipe-FaceMesh-0097A7)
8+
![License](https://img.shields.io/badge/license-MIT-green)
49

5-
<p align="center">
6-
<a href="https://ieeexplore.ieee.org/document/9532758"><img src="https://img.shields.io/badge/IEEE-Published%20Paper-00629B?style=for-the-badge&logo=ieee&logoColor=white" alt="IEEE Paper"></a>
7-
</p>
10+
Real-time driver drowsiness detection using the **Eye Aspect Ratio (EAR)**. Originally an IEEE conference paper; this repo packages it as a production-shaped Python library with a CLI, two interchangeable landmark backends, a stateful detector with event hooks, and a headless batch-analysis mode.
811

9-
<p align="center">
10-
<img src="https://img.shields.io/badge/Python-3.7+-3776AB?style=flat-square&logo=python&logoColor=white">
11-
<img src="https://img.shields.io/badge/OpenCV-4.x-5C3EE8?style=flat-square&logo=opencv&logoColor=white">
12-
<img src="https://img.shields.io/badge/dlib-face__recognition-green?style=flat-square">
13-
<img src="https://img.shields.io/badge/SciPy-EAR%20Computation-8CAAE6?style=flat-square&logo=scipy&logoColor=white">
14-
<img src="https://img.shields.io/badge/License-MIT-yellow?style=flat-square">
15-
</p>
12+
> 📄 S. Santhanam et al., *"Real-Time Drowsiness Detection using Computer Vision"*, **2021 5th ICCMC (IEEE)**. [Read the paper →](https://ieeexplore.ieee.org/document/9532758)
1613
1714
---
1815

19-
## 📄 Publication
16+
## What's new vs. the original script
2017

21-
This project is published in **IEEE Xplore**:
18+
| | Old script | This package |
19+
|---|---|---|
20+
| Architecture | Single file, globals, blocking alarm | Stateful `DrowsinessDetector` with FSM (`AWAKE`/`DROWSY`), event callbacks |
21+
| Alarm | Blocking — stalled the detection loop on every trigger | Background thread + cooldown (`Alarm`); never blocks frames |
22+
| Alarm spam | Fired every frame while eyes closed | Fires once per AWAKE→DROWSY transition |
23+
| Landmarks | dlib + `face_recognition` (CMake hell) | **MediaPipe FaceMesh by default** (pure-pip, faster, GPU-friendly) — `face_recognition` still available |
24+
| API | None | `DrowsinessDetector`, `DrowsinessConfig`, `DrowsyEvent`, `DetectionResult` |
25+
| CLI | Single mode | `drowsy run` (live) and `drowsy analyze` (headless batch → CSV/JSONL/JSON) |
26+
| Tests | None | 14 pytest cases, FSM and alarm fully covered, runs in CI on Python 3.9/3.11/3.12 |
27+
| Robustness | Crashed on missing alarm file; reset on lost detection | Soft-fails on missing audio; preserves state across brief detection dropouts |
28+
| Visualization | EAR text only | EAR vs threshold, FPS counter, color-coded eye polylines, drowsy banner |
2229

23-
> **S. Santhanam et al.**, "Real-Time Drowsiness Detection using Computer Vision," *2021 5th International Conference on Computing Methodologies and Communication (ICCMC)*, 2021.
24-
>
25-
> 🔗 **[Read the paper on IEEE Xplore →](https://ieeexplore.ieee.org/document/9532758)**
30+
---
31+
32+
## Install
33+
34+
```bash
35+
# Default install: package + OpenCV + NumPy
36+
pip install -e .
37+
38+
# Recommended: add MediaPipe (no native build, very fast)
39+
pip install -e ".[mediapipe,audio]"
40+
41+
# Or, original dlib-based backend (requires CMake)
42+
pip install -e ".[legacy,audio]"
43+
```
2644

2745
---
2846

29-
## 🧠 How It Works
47+
## Quick start
48+
49+
### CLI
50+
51+
```bash
52+
# Live webcam detection with overlay
53+
drowsy run 0
54+
55+
# Headless batch analysis of a video — emits CSV/JSONL/JSON report
56+
drowsy analyze drive.mp4 --output-dir report/
3057

31-
The system uses the **Eye Aspect Ratio (EAR)** to detect drowsiness in real-time:
58+
# Tune sensitivity
59+
drowsy run 0 --ear-threshold 0.22 --frame-count 24
3260

61+
# Use the legacy dlib backend
62+
drowsy run 0 --backend face_recognition
3363
```
34-
EAR = (||p2 - p6|| + ||p3 - p5||) / (2 × ||p1 - p4||)
64+
65+
Press **`q`** or **Esc** to quit live mode.
66+
67+
### Python API
68+
69+
```python
70+
import cv2
71+
from drowsiness import DrowsinessDetector, DrowsinessConfig
72+
73+
cfg = DrowsinessConfig(ear_threshold=0.23, closed_frames_to_alarm=24)
74+
75+
with DrowsinessDetector(cfg, on_event=lambda e: print("DROWSY!", e)) as detector:
76+
cap = cv2.VideoCapture(0)
77+
while True:
78+
ok, frame = cap.read()
79+
if not ok:
80+
break
81+
result = detector.process(frame)
82+
# result.state: EyeState.AWAKE | EyeState.DROWSY
83+
# result.ear: current Eye Aspect Ratio
84+
# result.event: DrowsyEvent or None (fires once per drowsy episode)
3585
```
3686

87+
---
88+
89+
## How it works
90+
91+
### Eye Aspect Ratio
92+
3793
```
94+
EAR = (||p2 - p6|| + ||p3 - p5||) / (2 × ||p1 - p4||)
95+
3896
p2 p3
3997
•--------•
4098
/ \
41-
p1 • • p4 ← horizontal axis
99+
p1 • • p4
42100
\ /
43101
•--------•
44102
p6 p5
45103
```
46104

47-
- **Eyes open** → EAR ≈ 0.3
105+
- **Eyes open** → EAR ≈ 0.30
48106
- **Eyes closed** → EAR ≈ 0.05
49-
- If EAR stays below a threshold for enough consecutive frames → **🚨 ALARM**
107+
- EAR below threshold for *N* consecutive frames → **drowsy event**
50108

51-
### Architecture
109+
### Detection FSM
52110

53111
```
54-
┌──────────┐ ┌──────────────┐ ┌───────────┐ ┌───────────┐
55-
│ Webcam │───▶│ Face Landmark │───▶│ Compute │───▶│ Trigger │
56-
Stream│ Detection EAR Alarm?
57-
└──────────┘ ──────────────┘ └───────────┘ └───────────┘
58-
face_recognition scipy playsound
112+
EAR < threshold (N frames)
113+
──────────┐ ─────────────────────────▶ ┌──────────┐
114+
AWAKE DROWSY
115+
└──────────┘ ───────────────────────────────────┘
116+
EAR ≥ threshold (M frames)
59117
```
60118

61-
---
62-
63-
## ✨ Features
64-
65-
- 🎥 Real-time webcam-based detection
66-
- 👁️ Eye Aspect Ratio (EAR) algorithm
67-
- 🔊 Audio alarm on drowsiness detection
68-
- 📊 Live EAR display overlay
69-
- ⚙️ Configurable thresholds via CLI args
70-
71-
---
72-
73-
## 🚀 Getting Started
119+
The state machine de-bounces both directions: a brief blink doesn't trigger an alarm, and a single open frame doesn't immediately clear a drowsy state.
74120

75-
### Prerequisites
121+
### Pipeline
76122

77-
- Python 3.7+
78-
- Webcam
79-
- CMake (for dlib): `brew install cmake` (macOS) or `apt install cmake` (Linux)
80-
81-
### Installation
82-
83-
```bash
84-
git clone https://github.com/Sanjays2402/Drowsiness-Detection-with-OpenCV.git
85-
cd Drowsiness-Detection-with-OpenCV
86-
pip install -r requirements.txt
123+
```
124+
Webcam / Video
125+
126+
127+
Landmark backend ──▶ 6 EAR points per eye
128+
(MediaPipe FaceMesh
129+
or face_recognition)
130+
131+
132+
EAR computation ──▶ average EAR
133+
134+
135+
Drowsy FSM ──▶ state, closed_frames, event?
136+
137+
├─ Alarm (background, debounced)
138+
├─ on_event callback (your code)
139+
└─ Visualization overlay
87140
```
88141

89-
### Usage
90-
91-
```bash
92-
# Default settings
93-
python drowsiness_detection.py
94-
95-
# Custom thresholds
96-
python drowsiness_detection.py --ear-threshold 0.22 --frame-count 48
142+
---
97143

98-
# Different video source
99-
python drowsiness_detection.py --video-source 1
144+
## Configuration
145+
146+
```python
147+
DrowsinessConfig(
148+
ear_threshold=0.25, # below = closed eye
149+
closed_frames_to_alarm=20, # frames below threshold → DROWSY
150+
open_frames_to_clear=5, # frames above threshold → AWAKE
151+
alarm_sound="assets/alert1.mp3",
152+
alarm_cooldown_s=3.0, # min seconds between alarm playbacks
153+
backend="mediapipe", # or "face_recognition"
154+
enable_alarm=True,
155+
)
100156
```
101157

102-
Press **`q`** to quit.
158+
| CLI flag | Default | Notes |
159+
|---|---|---|
160+
| `--ear-threshold` | `0.25` | Tune lower for darker/glasses-heavy footage. |
161+
| `--frame-count` | `20` | At 30 FPS this is ~0.7s of closed eyes. |
162+
| `--open-frames` | `5` | Clears the drowsy state after a sustained reopen. |
163+
| `--alarm-cooldown` | `3.0` | Seconds. Prevents back-to-back alarms. |
164+
| `--backend` | `mediapipe` | `mediapipe` or `face_recognition`. |
165+
| `--no-alarm` | off | Disables audio (good for benchmarking). |
166+
| `--no-window` | off | Headless `run` (no `cv2.imshow`). |
103167

104168
---
105169

106-
## ⚙️ Configuration
170+
## Batch analysis
107171

108-
| Parameter | Flag | Default | Description |
109-
|-----------|------|---------|-------------|
110-
| EAR Threshold | `--ear-threshold` | `0.25` | EAR below this = eyes closed |
111-
| Frame Count | `--frame-count` | `60` | Consecutive frames before alarm |
112-
| Video Source | `--video-source` | `0` | Webcam index |
113-
| Alarm Sound | `--alarm-sound` | `assets/alert1.mp3` | Path to alert audio |
172+
`drowsy analyze` runs a video through the same detector with audio disabled and produces a small report:
114173

115-
---
116-
117-
## 🎬 Demo
174+
```
175+
report/
176+
├── ear.csv # frame-by-frame EAR & state
177+
├── events.jsonl # one record per AWAKE→DROWSY transition
178+
└── summary.json # counts, parameters, timestamps
179+
```
118180

119-
> *Add a GIF or screenshot here!*
120-
>
121-
> ![Demo placeholder](https://via.placeholder.com/600x300?text=Add+Demo+GIF+Here)
181+
Useful for fleet review, evaluating threshold changes, or feeding downstream analytics without a GUI.
122182

123183
---
124184

125-
## 📁 Project Structure
185+
## Project structure
126186

127187
```
128-
├── drowsiness_detection.py # Main detection script
129-
├── assets/
130-
│ └── alert1.mp3 # Alarm sound
131-
├── requirements.txt
132-
├── LICENSE
133-
└── README.md
188+
drowsiness/
189+
├── __init__.py # public API
190+
├── ear.py # eye_aspect_ratio() + helpers
191+
├── landmarks.py # MediaPipe + face_recognition backends
192+
├── alarm.py # background, cooldown-debounced audio
193+
├── detector.py # DrowsinessDetector + FSM + events
194+
├── visualization.py # cv2 overlay (EAR, FPS, banner)
195+
└── cli.py # `drowsy` entrypoint
196+
tests/
197+
└── test_detector.py # 14 cases, mocked landmark backend
198+
assets/
199+
└── alert1.mp3
134200
```
135201

136202
---
137203

138-
## 🛠️ Tech Stack
204+
## Limitations
205+
206+
This is still an **EAR-based** system. Known weaknesses:
139207

140-
- **[OpenCV](https://opencv.org/)** — Video capture & frame processing
141-
- **[face_recognition](https://github.com/ageitgey/face_recognition)** — Facial landmark detection (dlib-based)
142-
- **[SciPy](https://scipy.org/)** — Euclidean distance for EAR
143-
- **[imutils](https://github.com/PyImageSearch/imutils)** — Video stream utilities
144-
- **[playsound](https://github.com/TaylorSMarks/playsound)** — Audio alarm playback
208+
- Sunglasses / heavy bangs / extreme head pose break landmark detection.
209+
- A single threshold doesn't fit every driver — production deployments should calibrate per-driver during the first ~30s of awake driving.
210+
- No yawn / head-nod / gaze-direction signal yet (PRs welcome — `DrowsyEvent` is the right place to fan out into multi-cue fusion).
211+
- No tracking across multiple faces; the first detected face is used. Cabin-facing cameras typically only see one driver.
145212

146213
---
147214

148-
## 📜 License
215+
## Citation
149216

150-
[MIT](LICENSE) © Sanjay Santhanam
217+
```bibtex
218+
@inproceedings{santhanam2021drowsiness,
219+
title = {Real-Time Drowsiness Detection using Computer Vision},
220+
author = {Santhanam, S. and others},
221+
booktitle = {2021 5th International Conference on Computing Methodologies and Communication (ICCMC)},
222+
year = {2021},
223+
doi = {10.1109/ICCMC51019.2021.9418325}
224+
}
225+
```
151226

152-
## 👤 Author
227+
## License
153228

154-
**Sanjay Santhanam**[GitHub](https://github.com/Sanjays2402)
229+
[MIT](LICENSE) © Sanjay Santhanam

drowsiness/__init__.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Real-time driver drowsiness detection.
2+
3+
Public API:
4+
5+
from drowsiness import DrowsinessDetector, DrowsinessConfig
6+
from drowsiness import EyeState, DrowsyEvent
7+
8+
The detector is backend-agnostic and stateful. See `drowsiness.cli` for
9+
the bundled `drowsy` command-line entry point.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
from .detector import (
15+
DrowsinessConfig,
16+
DrowsinessDetector,
17+
DrowsyEvent,
18+
DetectionResult,
19+
EyeState,
20+
)
21+
22+
__all__ = [
23+
"DrowsinessConfig",
24+
"DrowsinessDetector",
25+
"DrowsyEvent",
26+
"DetectionResult",
27+
"EyeState",
28+
"__version__",
29+
]
30+
31+
__version__ = "1.0.0"

0 commit comments

Comments
 (0)