Spaces:
Sleeping
Sleeping
Testimony Adekoya
commited on
Commit
·
19f420a
1
Parent(s):
bd08a17
Work on drive-paddy to huggingface
Browse files- .env.example +2 -0
- .gitattributes +1 -35
- Dockerfile +0 -21
- README.md +17 -5
- assets/alert.wav +0 -0
- assets/sleep.jpeg +0 -0
- config.yaml +49 -0
- download_model.py +42 -0
- main.py +72 -0
- models/best_model_efficientnet_b7.pth +3 -0
- pages/1_Live_Detection.py +153 -0
- requirements.txt +15 -3
- src/__init__.py +0 -0
- src/__pycache__/__init__.cpython-312.pyc +0 -0
- src/alerting/__init__.py +0 -0
- src/alerting/__pycache__/__init__.cpython-312.pyc +0 -0
- src/alerting/__pycache__/alert_system.cpython-312.pyc +0 -0
- src/alerting/alert_system.py +110 -0
- src/detection/__init__.py +0 -0
- src/detection/__pycache__/__init__.cpython-312.pyc +0 -0
- src/detection/__pycache__/base_processor.cpython-312.pyc +0 -0
- src/detection/__pycache__/factory.cpython-312.pyc +0 -0
- src/detection/base_processor.py +26 -0
- src/detection/factory.py +22 -0
- src/detection/strategies/__init__.py +0 -0
- src/detection/strategies/__pycache__/__init__.cpython-312.pyc +0 -0
- src/detection/strategies/__pycache__/cnn_model.cpython-312.pyc +0 -0
- src/detection/strategies/__pycache__/geometric.cpython-312.pyc +0 -0
- src/detection/strategies/__pycache__/hybrid.cpython-312.pyc +0 -0
- src/detection/strategies/cnn_model.py +100 -0
- src/detection/strategies/geometric.py +127 -0
- src/detection/strategies/hybrid.py +84 -0
- src/streamlit_app.py +0 -40
- utils.py +76 -0
.env.example
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
GEMINI_API_KEY=your_gemini_api_key_here
|
2 |
+
HUGGINGFACE_API_KEY=your_huggingface_api_key_here
|
.gitattributes
CHANGED
@@ -1,35 +1 @@
|
|
1 |
-
|
2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
1 |
+
models/best_model_efficientnet_b7.pth filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
CHANGED
@@ -1,21 +0,0 @@
|
|
1 |
-
FROM python:3.9-slim
|
2 |
-
|
3 |
-
WORKDIR /app
|
4 |
-
|
5 |
-
RUN apt-get update && apt-get install -y \
|
6 |
-
build-essential \
|
7 |
-
curl \
|
8 |
-
software-properties-common \
|
9 |
-
git \
|
10 |
-
&& rm -rf /var/lib/apt/lists/*
|
11 |
-
|
12 |
-
COPY requirements.txt ./
|
13 |
-
COPY src/ ./src/
|
14 |
-
|
15 |
-
RUN pip3 install -r requirements.txt
|
16 |
-
|
17 |
-
EXPOSE 8501
|
18 |
-
|
19 |
-
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
|
20 |
-
|
21 |
-
ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
@@ -8,12 +8,24 @@ app_port: 8501
|
|
8 |
tags:
|
9 |
- streamlit
|
10 |
pinned: false
|
11 |
-
short_description:
|
12 |
---
|
13 |
|
14 |
-
#
|
15 |
|
16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
-
If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
|
19 |
-
forums](https://discuss.streamlit.io).
|
|
|
8 |
tags:
|
9 |
- streamlit
|
10 |
pinned: false
|
11 |
+
short_description: Drive Paddy is a drowsiness detection buddy for drivers, utilizing OpenCV and a fine-tuned CNN model to monitor driver alertness.
|
12 |
---
|
13 |
|
14 |
+
# Drive Paddy 🚀
|
15 |
|
16 |
+
Drive Paddy is a system designed to enhance driver safety by detecting drowsiness. It utilizes **OpenCV** for real-time computer vision tasks and a **fine-tuned Convolutional Neural Network (CNN)** model to monitor driver alertness and help prevent fatigue-related incidents.
|
17 |
+
|
18 |
+
## Core Functionality
|
19 |
+
|
20 |
+
* **Drowsiness Detection:** Identifies signs of driver fatigue by analyzing visual cues from a camera feed, such as eye closure duration and head pose.
|
21 |
+
* **Real-time Monitoring:** Continuously processes video input to assess the driver's alertness level.
|
22 |
+
* **Computer Vision Engine:** Leverages OpenCV for robust facial feature detection and eye state analysis.
|
23 |
+
* **AI-Powered Classification:** Employs a fine-tuned CNN model for accurate determination of drowsiness.
|
24 |
+
|
25 |
+
## Technology Stack
|
26 |
+
|
27 |
+
* **Computer Vision:** OpenCV
|
28 |
+
* **Deep Learning Model:** Fine-tuned Convolutional Neural Network (CNN)
|
29 |
+
* **Application Framework:** Streamlit (as indicated by project metadata `tags`)
|
30 |
+
* **Containerization:** Docker (as indicated by project metadata `sdk`)
|
31 |
|
|
|
|
assets/alert.wav
ADDED
File without changes
|
assets/sleep.jpeg
ADDED
![]() |
config.yaml
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# config.yaml
|
2 |
+
# Main configuration file for the Drive Paddy application.
|
3 |
+
|
4 |
+
# -- Detection Strategy --
|
5 |
+
# Sets the active drowsiness detection method.
|
6 |
+
# Options: "geometric", "cnn_model", "hybrid"
|
7 |
+
detection_strategy: "geometric"
|
8 |
+
|
9 |
+
# -- Geometric Strategy Settings --
|
10 |
+
# Parameters for the facial landmark-based detection methods.
|
11 |
+
geometric_settings:
|
12 |
+
# Eye Aspect Ratio (EAR) for blink/closure detection
|
13 |
+
eye_ar_thresh: 0.23
|
14 |
+
eye_ar_consec_frames: 15
|
15 |
+
|
16 |
+
# Mouth Aspect Ratio (MAR) for yawn detection
|
17 |
+
yawn_mar_thresh: 0.70
|
18 |
+
yawn_consec_frames: 20
|
19 |
+
|
20 |
+
# Head Pose Estimation for look-away/nod-off detection
|
21 |
+
head_nod_thresh: 15.0 # Max downward pitch angle (in degrees)
|
22 |
+
head_look_away_thresh: 20.0 # Max yaw angle (in degrees)
|
23 |
+
head_pose_consec_frames: 20
|
24 |
+
|
25 |
+
# -- CNN Model Settings --
|
26 |
+
cnn_model_settings:
|
27 |
+
model_path: "models/best_model_efficientnet_b7.pth"
|
28 |
+
confidence_thresh: 0.8
|
29 |
+
|
30 |
+
# -- Hybrid Strategy Settings --
|
31 |
+
# Defines weights for combining signals into a single drowsiness score.
|
32 |
+
# The system triggers an alert if the total score exceeds 'alert_threshold'.
|
33 |
+
hybrid_settings:
|
34 |
+
alert_threshold: 1.0
|
35 |
+
weights:
|
36 |
+
eye_closure: 0.45
|
37 |
+
yawning: 0.30
|
38 |
+
head_nod: 0.55
|
39 |
+
looking_away: 0.25
|
40 |
+
cnn_prediction: 0.60 # Weight for the deep learning model's output
|
41 |
+
|
42 |
+
# -- Alerting System --
|
43 |
+
alerting:
|
44 |
+
alert_sound_path: "assets/alert.wav"
|
45 |
+
alert_cooldown_seconds: 5
|
46 |
+
|
47 |
+
# -- Gemini API (Optional) --
|
48 |
+
gemini_api:
|
49 |
+
enabled: true
|
download_model.py
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# download_model.py
|
2 |
+
import os
|
3 |
+
from huggingface_hub import hf_hub_download
|
4 |
+
|
5 |
+
# --- Configuration ---
|
6 |
+
# Details from your Hugging Face repository screenshot.
|
7 |
+
REPO_ID = "Testys/drowsiness-detection-model"
|
8 |
+
FILENAME = "best_model_efficientnet_b7.pth"
|
9 |
+
LOCAL_DIR = "models"
|
10 |
+
|
11 |
+
def download_model():
|
12 |
+
"""
|
13 |
+
Downloads the specified model file from Hugging Face Hub
|
14 |
+
and saves it to the local models/ directory.
|
15 |
+
"""
|
16 |
+
print(f"Downloading model '{FILENAME}' from repository '{REPO_ID}'...")
|
17 |
+
|
18 |
+
# Ensure the local directory exists.
|
19 |
+
if not os.path.exists(LOCAL_DIR):
|
20 |
+
os.makedirs(LOCAL_DIR)
|
21 |
+
print(f"Created directory: {LOCAL_DIR}")
|
22 |
+
|
23 |
+
try:
|
24 |
+
# Download the file.
|
25 |
+
# local_dir_use_symlinks=False ensures the file is copied to your directory
|
26 |
+
# instead of just pointing to the cache.
|
27 |
+
model_path = hf_hub_download(
|
28 |
+
repo_id=REPO_ID,
|
29 |
+
filename=FILENAME,
|
30 |
+
local_dir=LOCAL_DIR,
|
31 |
+
local_dir_use_symlinks=False,
|
32 |
+
# token=True # Use token for private repos, can be omitted for public ones
|
33 |
+
)
|
34 |
+
print(f"\nModel downloaded successfully!")
|
35 |
+
print(f"Saved to: {model_path}")
|
36 |
+
|
37 |
+
except Exception as e:
|
38 |
+
print(f"\nAn error occurred during download: {e}")
|
39 |
+
print("Please check the repository ID, filename, and your network connection.")
|
40 |
+
|
41 |
+
if __name__ == "__main__":
|
42 |
+
download_model()
|
main.py
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# drive_paddy/main.py
|
2 |
+
import streamlit as st
|
3 |
+
import yaml
|
4 |
+
import os
|
5 |
+
from dotenv import load_dotenv
|
6 |
+
|
7 |
+
# --- Main Application UI ---
|
8 |
+
st.set_page_config(
|
9 |
+
page_title="Drive Paddy | Home",
|
10 |
+
page_icon="🚗",
|
11 |
+
layout="wide"
|
12 |
+
)
|
13 |
+
|
14 |
+
# Load config to display current settings on the home page
|
15 |
+
@st.cache_resource
|
16 |
+
def load_app_config():
|
17 |
+
load_dotenv()
|
18 |
+
gemini_api_key = os.getenv("GEMINI_API_KEY")
|
19 |
+
with open('config.yaml', 'r') as f:
|
20 |
+
config = yaml.safe_load(f)
|
21 |
+
return config, gemini_api_key
|
22 |
+
|
23 |
+
config, gemini_api_key = load_app_config()
|
24 |
+
|
25 |
+
# --- Initialize Session State ---
|
26 |
+
# This ensures they are set when the app first loads.
|
27 |
+
if "play_audio" not in st.session_state:
|
28 |
+
st.session_state.play_audio = None
|
29 |
+
if "active_alerts" not in st.session_state:
|
30 |
+
st.session_state.active_alerts = {"status": "Awake"}
|
31 |
+
|
32 |
+
|
33 |
+
# --- Page Content ---
|
34 |
+
st.title("🚗 Welcome to Drive Paddy!")
|
35 |
+
st.subheader("Your AI-Powered Drowsiness Detection Assistant")
|
36 |
+
|
37 |
+
st.markdown("""
|
38 |
+
Drive Paddy is a real-time system designed to enhance driver safety by detecting signs of drowsiness.
|
39 |
+
It uses your computer's webcam to analyze facial features and head movements, providing timely alerts
|
40 |
+
to help prevent fatigue-related accidents.
|
41 |
+
""")
|
42 |
+
|
43 |
+
st.info("Navigate to the **Live Detection** page from the sidebar on the left to start the system.")
|
44 |
+
|
45 |
+
st.markdown("---")
|
46 |
+
|
47 |
+
col1, col2 = st.columns(2)
|
48 |
+
|
49 |
+
with col1:
|
50 |
+
st.header("How It Works")
|
51 |
+
st.markdown("""
|
52 |
+
The system employs a sophisticated hybrid strategy to monitor for signs of fatigue:
|
53 |
+
- **👀 Eye Closure Detection**: Measures Eye Aspect Ratio (EAR) to detect prolonged blinks or closed eyes.
|
54 |
+
- **🥱 Yawn Detection**: Measures Mouth Aspect Ratio (MAR) to identify yawns.
|
55 |
+
- **😴 Head Pose Analysis**: Tracks head pitch and yaw to detect nodding off or looking away from the road.
|
56 |
+
- **🧠 CNN Model Inference**: A deep learning model provides an additional layer of analysis.
|
57 |
+
|
58 |
+
These signals are combined into a single drowsiness score to trigger alerts accurately.
|
59 |
+
""")
|
60 |
+
|
61 |
+
with col2:
|
62 |
+
st.header("Current Configuration")
|
63 |
+
alert_method = "Gemini API" if config.get('gemini_api', {}).get('enabled') and gemini_api_key else "Static Audio File"
|
64 |
+
st.markdown(f"""
|
65 |
+
- **Detection Strategy**: `{config['detection_strategy']}`
|
66 |
+
- **Alert Method**: `{alert_method}`
|
67 |
+
""")
|
68 |
+
st.warning("Ensure good lighting and that your face is clearly visible for best results.")
|
69 |
+
|
70 |
+
st.markdown("---")
|
71 |
+
st.markdown("Created with ❤️ using Streamlit, OpenCV, and MediaPipe.")
|
72 |
+
|
models/best_model_efficientnet_b7.pth
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:b13c1e5e4f1a03e0e559ad8f7988c14b63d2b028c55f380814f241dd788a99df
|
3 |
+
size 256870774
|
pages/1_Live_Detection.py
ADDED
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# drive_paddy/pages/1_Live_Detection.py
|
2 |
+
import streamlit as st
|
3 |
+
from streamlit_webrtc import webrtc_streamer, RTCConfiguration, VideoProcessorBase
|
4 |
+
import yaml
|
5 |
+
import av
|
6 |
+
import os
|
7 |
+
from dotenv import load_dotenv
|
8 |
+
import base64
|
9 |
+
import queue
|
10 |
+
import time
|
11 |
+
|
12 |
+
from src.detection.factory import get_detector
|
13 |
+
from src.alerting.alert_system import get_alerter
|
14 |
+
|
15 |
+
# --- Load Configuration and Environment Variables ---
|
16 |
+
@st.cache_resource
|
17 |
+
def load_app_config():
|
18 |
+
"""Loads config from yaml and .env files."""
|
19 |
+
load_dotenv()
|
20 |
+
gemini_api_key = os.getenv("GEMINI_API_KEY")
|
21 |
+
# Navigate up to the root to find the config file
|
22 |
+
config_path = "/config.yaml" if os.path.exists("/config.yaml") else "config.yaml"
|
23 |
+
with open(config_path, 'r') as f:
|
24 |
+
config = yaml.safe_load(f)
|
25 |
+
return config, gemini_api_key
|
26 |
+
|
27 |
+
config, gemini_api_key = load_app_config()
|
28 |
+
|
29 |
+
# --- Initialize Session State (if not already done in main.py) ---
|
30 |
+
if "play_audio" not in st.session_state:
|
31 |
+
st.session_state.play_audio = None
|
32 |
+
if "active_alerts" not in st.session_state:
|
33 |
+
st.session_state.active_alerts = {"status": "Awake"}
|
34 |
+
|
35 |
+
# --- Client-Side Audio Playback Function ---
|
36 |
+
def autoplay_audio(audio_bytes: bytes):
|
37 |
+
"""Injects HTML to autoplay audio in the user's browser."""
|
38 |
+
b64 = base64.b64encode(audio_bytes).decode()
|
39 |
+
md = f"""
|
40 |
+
<audio controls autoplay="true" style="display:none;">
|
41 |
+
<source src="data:audio/mp3;base64,{b64}" type="audio/mp3">
|
42 |
+
</audio>
|
43 |
+
"""
|
44 |
+
st.markdown(md, unsafe_allow_html=True)
|
45 |
+
|
46 |
+
# --- WebRTC Video Processor ---
|
47 |
+
class VideoProcessor(VideoProcessorBase):
|
48 |
+
def __init__(self):
|
49 |
+
self._detector = get_detector(config)
|
50 |
+
self._alerter = get_alerter(config, gemini_api_key)
|
51 |
+
|
52 |
+
def recv(self, frame: av.VideoFrame) -> av.VideoFrame:
|
53 |
+
img = frame.to_ndarray(format="bgr24")
|
54 |
+
|
55 |
+
strategy = config.get('detection_strategy')
|
56 |
+
if strategy == 'hybrid':
|
57 |
+
processed_frame, alert_triggered, active_alerts = self._detector.process_frame(img)
|
58 |
+
st.session_state.active_alerts = active_alerts if alert_triggered else {"status": "Awake"}
|
59 |
+
else: # Fallback for simpler strategies
|
60 |
+
processed_frame, indicators = self._detector.process_frame(img)
|
61 |
+
alert_triggered = any(indicators.values())
|
62 |
+
st.session_state.active_alerts = indicators if alert_triggered else {"status": "Awake"}
|
63 |
+
|
64 |
+
if alert_triggered:
|
65 |
+
audio_data = self._alerter.trigger_alert()
|
66 |
+
if audio_data:
|
67 |
+
st.session_state.play_audio = audio_data
|
68 |
+
else:
|
69 |
+
self._alerter.reset_alert()
|
70 |
+
|
71 |
+
return av.VideoFrame.from_ndarray(processed_frame, format="bgr24")
|
72 |
+
|
73 |
+
# --- Page UI ---
|
74 |
+
# The st.set_page_config() call has been removed from this file.
|
75 |
+
# The configuration from main.py will apply to this page.
|
76 |
+
st.title("📹 Live Drowsiness Detection")
|
77 |
+
st.info("Press 'START' to activate your camera and begin monitoring.")
|
78 |
+
|
79 |
+
# --- Robust RTC Configuration ---
|
80 |
+
# Provide a list of STUN servers for better reliability.
|
81 |
+
RTC_CONFIGURATION = RTCConfiguration({
|
82 |
+
"iceServers": [
|
83 |
+
{"urls": ["stun:stun.l.google.com:19302"]},
|
84 |
+
{"urls": ["stun:stun1.l.google.com:19302"]},
|
85 |
+
{"urls": ["stun:stun2.l.google.com:19302"]},
|
86 |
+
{"urls": ["stun:stun.services.mozilla.com:3478"]},
|
87 |
+
]
|
88 |
+
})
|
89 |
+
|
90 |
+
|
91 |
+
col1, col2 = st.columns([3, 1])
|
92 |
+
|
93 |
+
with col1:
|
94 |
+
webrtc_ctx = webrtc_streamer(
|
95 |
+
key="drowsiness-detection",
|
96 |
+
video_processor_factory=VideoProcessor,
|
97 |
+
rtc_configuration=RTC_CONFIGURATION, # Use the new robust configuration
|
98 |
+
media_stream_constraints={"video": True, "audio": False},
|
99 |
+
async_processing=True,
|
100 |
+
)
|
101 |
+
|
102 |
+
with col2:
|
103 |
+
st.header("System Status")
|
104 |
+
if not webrtc_ctx.state.playing:
|
105 |
+
st.warning("System Inactive.")
|
106 |
+
else:
|
107 |
+
st.success("✅ System Active & Monitoring")
|
108 |
+
|
109 |
+
st.subheader("Live Status:")
|
110 |
+
status_placeholder = st.empty()
|
111 |
+
audio_placeholder = st.empty()
|
112 |
+
|
113 |
+
if webrtc_ctx.state.playing:
|
114 |
+
# --- Polling Loop ---
|
115 |
+
try:
|
116 |
+
status_result = st.session_state.status_queue.get(timeout=0.1)
|
117 |
+
except queue.Empty:
|
118 |
+
status_result = None
|
119 |
+
|
120 |
+
# Check for new audio alerts
|
121 |
+
try:
|
122 |
+
audio_data = st.session_state.audio_queue.get(timeout=0.1)
|
123 |
+
except queue.Empty:
|
124 |
+
audio_data = None
|
125 |
+
|
126 |
+
with status_placeholder.container():
|
127 |
+
# Persist the last known status if there's no new one
|
128 |
+
if status_result:
|
129 |
+
st.session_state.last_status = status_result
|
130 |
+
|
131 |
+
last_status = getattr(st.session_state, 'last_status', {"status": "Awake"})
|
132 |
+
|
133 |
+
if last_status.get("Low Light"):
|
134 |
+
st.warning("⚠️ Low Light Detected! Accuracy may be affected.")
|
135 |
+
elif last_status.get("status") == "Awake":
|
136 |
+
st.info("✔️ Driver is Awake")
|
137 |
+
else:
|
138 |
+
st.error("🚨 DROWSINESS DETECTED!")
|
139 |
+
for key, value in last_status.items():
|
140 |
+
if key != "Low Light":
|
141 |
+
st.warning(f"-> {key}: {value:.2f}" if isinstance(value, float) else f"-> {key}")
|
142 |
+
|
143 |
+
if audio_data:
|
144 |
+
with audio_placeholder.container():
|
145 |
+
autoplay_audio(audio_data)
|
146 |
+
|
147 |
+
# Force a rerun to keep the polling active
|
148 |
+
time.sleep(0.1)
|
149 |
+
st.rerun()
|
150 |
+
|
151 |
+
else:
|
152 |
+
with status_placeholder.container():
|
153 |
+
st.info("✔️ Driver is Awake")
|
requirements.txt
CHANGED
@@ -1,3 +1,15 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
streamlit
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
huggingface-hub
|
2 |
+
streamlit
|
3 |
+
streamlit-webrtc
|
4 |
+
opencv-contrib-python
|
5 |
+
mediapipe
|
6 |
+
numpy
|
7 |
+
pyyaml
|
8 |
+
simpleaudio
|
9 |
+
pydub
|
10 |
+
python-dotenv
|
11 |
+
google-generativeai
|
12 |
+
gTTS
|
13 |
+
torch
|
14 |
+
torchvision
|
15 |
+
dlib
|
src/__init__.py
ADDED
File without changes
|
src/__pycache__/__init__.cpython-312.pyc
ADDED
Binary file (136 Bytes). View file
|
|
src/alerting/__init__.py
ADDED
File without changes
|
src/alerting/__pycache__/__init__.cpython-312.pyc
ADDED
Binary file (145 Bytes). View file
|
|
src/alerting/__pycache__/alert_system.cpython-312.pyc
ADDED
Binary file (6.55 kB). View file
|
|
src/alerting/alert_system.py
ADDED
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# drive_paddy/alerting/alert_system.py
|
2 |
+
import time
|
3 |
+
import os
|
4 |
+
import io
|
5 |
+
from gtts import gTTS
|
6 |
+
import google.generativeai as genai
|
7 |
+
from dotenv import load_dotenv
|
8 |
+
|
9 |
+
load_dotenv() # Load environment variables from .env file
|
10 |
+
|
11 |
+
api_key = os.getenv("GEMINI_API_KEY")
|
12 |
+
class BaseAlerter:
|
13 |
+
"""Base class for alerter systems."""
|
14 |
+
def __init__(self, config):
|
15 |
+
self.config = config['alerting']
|
16 |
+
self.cooldown = self.config['alert_cooldown_seconds']
|
17 |
+
self.last_alert_time = 0
|
18 |
+
self.alert_on = False
|
19 |
+
|
20 |
+
def trigger_alert(self):
|
21 |
+
raise NotImplementedError
|
22 |
+
|
23 |
+
def reset_alert(self):
|
24 |
+
if self.alert_on:
|
25 |
+
print("Resetting Alert.")
|
26 |
+
self.alert_on = False
|
27 |
+
|
28 |
+
class FileAlertSystem(BaseAlerter):
|
29 |
+
"""Loads a static audio file from disk into memory."""
|
30 |
+
def __init__(self, config):
|
31 |
+
super().__init__(config)
|
32 |
+
self.sound_path = self.config['alert_sound_path']
|
33 |
+
self.audio_bytes = None
|
34 |
+
try:
|
35 |
+
if os.path.exists(self.sound_path):
|
36 |
+
with open(self.sound_path, "rb") as f:
|
37 |
+
self.audio_bytes = f.read()
|
38 |
+
else:
|
39 |
+
print(f"Warning: Alert sound file not found at '{self.sound_path}'.")
|
40 |
+
except Exception as e:
|
41 |
+
print(f"Warning: Could not load audio file. Error: {e}.")
|
42 |
+
|
43 |
+
def trigger_alert(self):
|
44 |
+
current_time = time.time()
|
45 |
+
if (current_time - self.last_alert_time) > self.cooldown:
|
46 |
+
if not self.alert_on and self.audio_bytes:
|
47 |
+
print("Triggering Static Alert!")
|
48 |
+
self.last_alert_time = current_time
|
49 |
+
self.alert_on = True
|
50 |
+
return self.audio_bytes # Return the audio data
|
51 |
+
return None
|
52 |
+
|
53 |
+
|
54 |
+
class GeminiAlertSystem(BaseAlerter):
|
55 |
+
"""Generates dynamic audio data using Gemini and gTTS."""
|
56 |
+
def __init__(self, config, api_key):
|
57 |
+
super().__init__(config)
|
58 |
+
try:
|
59 |
+
genai.configure(api_key=api_key)
|
60 |
+
self.model = genai.GenerativeModel('gemini-1.5-flash') # Use the Gemini model
|
61 |
+
print("Gemini Alert System initialized successfully.")
|
62 |
+
except Exception as e:
|
63 |
+
print(f"Error initializing Gemini: {e}.")
|
64 |
+
self.model = None
|
65 |
+
|
66 |
+
def _generate_audio_data(self):
|
67 |
+
"""Generates a unique alert message and returns it as audio bytes."""
|
68 |
+
if not self.model:
|
69 |
+
alert_text = "Stay alert!"
|
70 |
+
else:
|
71 |
+
prompt = "You are an AI driving assistant. Generate a short, friendly, but firm audio alert (under 10 words) for a driver showing signs of drowsiness."
|
72 |
+
try:
|
73 |
+
response = self.model.generate_content(prompt)
|
74 |
+
alert_text = response.text.strip().replace('*', '')
|
75 |
+
except Exception as e:
|
76 |
+
print(f"Error generating alert text with Gemini: {e}")
|
77 |
+
alert_text = "Wake up please!"
|
78 |
+
|
79 |
+
print(f"Generated Alert Text: '{alert_text}'")
|
80 |
+
try:
|
81 |
+
# Generate TTS audio in memory
|
82 |
+
mp3_fp = io.BytesIO()
|
83 |
+
tts = gTTS(text=alert_text, lang='en')
|
84 |
+
tts.write_to_fp(mp3_fp)
|
85 |
+
mp3_fp.seek(0)
|
86 |
+
return mp3_fp.getvalue()
|
87 |
+
except Exception as e:
|
88 |
+
print(f"Error generating TTS audio: {e}")
|
89 |
+
return None
|
90 |
+
|
91 |
+
def trigger_alert(self):
|
92 |
+
current_time = time.time()
|
93 |
+
if (current_time - self.last_alert_time) > self.cooldown:
|
94 |
+
if not self.alert_on and self.model:
|
95 |
+
self.last_alert_time = current_time
|
96 |
+
self.alert_on = True
|
97 |
+
return self._generate_audio_data() # Return the audio data
|
98 |
+
return None
|
99 |
+
|
100 |
+
|
101 |
+
def get_alerter(config, api_key=None):
|
102 |
+
"""Factory to get the appropriate alerter based on config."""
|
103 |
+
use_gemini = config.get('gemini_api', {}).get('enabled', False)
|
104 |
+
|
105 |
+
if use_gemini and api_key:
|
106 |
+
print("Initializing Gemini Alert System.")
|
107 |
+
return GeminiAlertSystem(config, api_key)
|
108 |
+
else:
|
109 |
+
print("Initializing standard File Alert System.")
|
110 |
+
return FileAlertSystem(config)
|
src/detection/__init__.py
ADDED
File without changes
|
src/detection/__pycache__/__init__.cpython-312.pyc
ADDED
Binary file (146 Bytes). View file
|
|
src/detection/__pycache__/base_processor.cpython-312.pyc
ADDED
Binary file (1.12 kB). View file
|
|
src/detection/__pycache__/factory.cpython-312.pyc
ADDED
Binary file (1.18 kB). View file
|
|
src/detection/base_processor.py
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# drive_paddy/detection/base_processor.py
|
2 |
+
from abc import ABC, abstractmethod
|
3 |
+
|
4 |
+
class BaseProcessor(ABC):
|
5 |
+
"""
|
6 |
+
Abstract Base Class for a drowsiness detection processor.
|
7 |
+
|
8 |
+
This defines the common interface that all detection strategies
|
9 |
+
(e.g., Geometric, CNN Model) must follow.
|
10 |
+
"""
|
11 |
+
|
12 |
+
@abstractmethod
|
13 |
+
def process_frame(self, frame):
|
14 |
+
"""
|
15 |
+
Processes a single video frame to detect drowsiness.
|
16 |
+
|
17 |
+
Args:
|
18 |
+
frame: The video frame (as a NumPy array) to process.
|
19 |
+
|
20 |
+
Returns:
|
21 |
+
A tuple containing:
|
22 |
+
- The processed frame (NumPy array) with visualizations.
|
23 |
+
- A boolean indicating if an alert should be triggered.
|
24 |
+
"""
|
25 |
+
pass
|
26 |
+
|
src/detection/factory.py
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# drive_paddy/detection/factory.py
|
2 |
+
from src.detection.strategies.geometric import GeometricProcessor
|
3 |
+
from src.detection.strategies.cnn_model import CnnProcessor
|
4 |
+
from src.detection.strategies.hybrid import HybridProcessor
|
5 |
+
|
6 |
+
def get_detector(config):
|
7 |
+
"""
|
8 |
+
Factory function to get the appropriate drowsiness detector.
|
9 |
+
"""
|
10 |
+
strategy = config.get('detection_strategy', 'geometric')
|
11 |
+
|
12 |
+
if strategy == 'geometric':
|
13 |
+
print("Initializing Geometric drowsiness detector...")
|
14 |
+
return GeometricProcessor(config)
|
15 |
+
elif strategy == 'cnn_model':
|
16 |
+
print("Initializing CNN Model drowsiness detector...")
|
17 |
+
return CnnProcessor(config)
|
18 |
+
elif strategy == 'hybrid':
|
19 |
+
print("Initializing Hybrid (Geometric + CNN) drowsiness detector...")
|
20 |
+
return HybridProcessor(config)
|
21 |
+
else:
|
22 |
+
raise ValueError(f"Unknown detection strategy: {strategy}")
|
src/detection/strategies/__init__.py
ADDED
File without changes
|
src/detection/strategies/__pycache__/__init__.cpython-312.pyc
ADDED
Binary file (157 Bytes). View file
|
|
src/detection/strategies/__pycache__/cnn_model.cpython-312.pyc
ADDED
Binary file (5.3 kB). View file
|
|
src/detection/strategies/__pycache__/geometric.cpython-312.pyc
ADDED
Binary file (7.89 kB). View file
|
|
src/detection/strategies/__pycache__/hybrid.cpython-312.pyc
ADDED
Binary file (4.97 kB). View file
|
|
src/detection/strategies/cnn_model.py
ADDED
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# drive_paddy/detection/strategies/cnn_model.py
|
2 |
+
from src.detection.base_processor import BaseProcessor
|
3 |
+
import numpy as np
|
4 |
+
import torch
|
5 |
+
import torchvision.transforms as transforms
|
6 |
+
from torchvision.models import efficientnet_b7
|
7 |
+
import cv2
|
8 |
+
import dlib
|
9 |
+
from PIL import Image
|
10 |
+
import os
|
11 |
+
|
12 |
+
class CnnProcessor(BaseProcessor):
|
13 |
+
"""
|
14 |
+
Drowsiness detection using a pre-trained EfficientNet-B7 model.
|
15 |
+
"""
|
16 |
+
def __init__(self, config):
|
17 |
+
self.settings = config['cnn_model_settings']
|
18 |
+
self.model_path = self.settings['model_path']
|
19 |
+
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
20 |
+
|
21 |
+
# Initialize dlib for face detection
|
22 |
+
self.face_detector = dlib.get_frontal_face_detector()
|
23 |
+
|
24 |
+
# Load the model
|
25 |
+
self.model = self._load_model()
|
26 |
+
|
27 |
+
# Define image transformations
|
28 |
+
self.transform = transforms.Compose([
|
29 |
+
transforms.Resize((224, 224)),
|
30 |
+
transforms.ToTensor(),
|
31 |
+
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
|
32 |
+
])
|
33 |
+
|
34 |
+
def _load_model(self):
|
35 |
+
"""Loads the EfficientNet-B7 model and custom weights."""
|
36 |
+
if not os.path.exists(self.model_path):
|
37 |
+
print(f"Error: Model file not found at {self.model_path}")
|
38 |
+
print("Please run 'python download_model.py' first.")
|
39 |
+
return None
|
40 |
+
|
41 |
+
try:
|
42 |
+
# Initialize the model structure
|
43 |
+
model = efficientnet_b7()
|
44 |
+
# Modify the final classifier layer to match the number of output classes (e.g., 2: drowsy, not_drowsy)
|
45 |
+
num_ftrs = model.classifier[1].in_features
|
46 |
+
model.classifier[1] = torch.nn.Linear(num_ftrs, 2) # Assuming 2 output classes
|
47 |
+
|
48 |
+
# Load the saved weights
|
49 |
+
model.load_state_dict(torch.load(self.model_path, map_location=self.device))
|
50 |
+
model.to(self.device)
|
51 |
+
model.eval() # Set the model to evaluation mode
|
52 |
+
print(f"CNN Model '{self.model_path}' loaded successfully on {self.device}.")
|
53 |
+
return model
|
54 |
+
except Exception as e:
|
55 |
+
print(f"Error loading CNN model: {e}")
|
56 |
+
return None
|
57 |
+
|
58 |
+
def process_frame(self, frame):
|
59 |
+
"""
|
60 |
+
Processes a frame to detect drowsiness using the CNN model.
|
61 |
+
"""
|
62 |
+
if self.model is None:
|
63 |
+
return frame, {"cnn_prediction": False}
|
64 |
+
|
65 |
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
66 |
+
faces = self.face_detector(gray)
|
67 |
+
is_drowsy_prediction = False
|
68 |
+
|
69 |
+
for face in faces:
|
70 |
+
x1, y1, x2, y2 = face.left(), face.top(), face.right(), face.bottom()
|
71 |
+
|
72 |
+
# Crop the face from the frame
|
73 |
+
face_crop = frame[y1:y2, x1:x2]
|
74 |
+
|
75 |
+
# Ensure the crop is valid before processing
|
76 |
+
if face_crop.size == 0:
|
77 |
+
continue
|
78 |
+
|
79 |
+
# Convert to PIL Image and apply transformations
|
80 |
+
pil_image = Image.fromarray(cv2.cvtColor(face_crop, cv2.COLOR_BGR2RGB))
|
81 |
+
image_tensor = self.transform(pil_image).unsqueeze(0).to(self.device)
|
82 |
+
|
83 |
+
# Perform inference
|
84 |
+
with torch.no_grad():
|
85 |
+
outputs = self.model(image_tensor)
|
86 |
+
_, preds = torch.max(outputs, 1)
|
87 |
+
# Assuming class 1 is 'drowsy' and class 0 is 'not_drowsy'
|
88 |
+
print(preds)
|
89 |
+
if preds.item() == 1:
|
90 |
+
is_drowsy_prediction = True
|
91 |
+
|
92 |
+
# Draw bounding box for visualization
|
93 |
+
cv2.rectangle(frame, (x1, y1), (x2, y2), (255, 255, 0), 2)
|
94 |
+
label = "Drowsy" if is_drowsy_prediction else "Awake"
|
95 |
+
cv2.putText(frame, f"CNN: {label}", (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 0), 2)
|
96 |
+
|
97 |
+
# Process only the first detected face
|
98 |
+
break
|
99 |
+
|
100 |
+
return frame, {"cnn_prediction": is_drowsy_prediction}
|
src/detection/strategies/geometric.py
ADDED
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# drive_paddy/detection/strategies/geometric.py
|
2 |
+
import cv2
|
3 |
+
import mediapipe as mp
|
4 |
+
import numpy as np
|
5 |
+
import math
|
6 |
+
from ..base_processor import BaseProcessor
|
7 |
+
|
8 |
+
# --- Helper Functions ---
|
9 |
+
def calculate_ear(eye_landmarks, frame_shape):
|
10 |
+
"""Calculates the Eye Aspect Ratio (EAR)."""
|
11 |
+
# ... (implementation remains the same)
|
12 |
+
coords = np.array([(lm.x * frame_shape[1], lm.y * frame_shape[0]) for lm in eye_landmarks])
|
13 |
+
v1 = np.linalg.norm(coords[1] - coords[5]); v2 = np.linalg.norm(coords[2] - coords[4])
|
14 |
+
h1 = np.linalg.norm(coords[0] - coords[3])
|
15 |
+
return (v1 + v2) / (2.0 * h1) if h1 > 0 else 0.0
|
16 |
+
|
17 |
+
def calculate_mar(mouth_landmarks, frame_shape):
|
18 |
+
"""Calculates the Mouth Aspect Ratio (MAR) for yawn detection."""
|
19 |
+
coords = np.array([(lm.x * frame_shape[1], lm.y * frame_shape[0]) for lm in mouth_landmarks])
|
20 |
+
v1 = np.linalg.norm(coords[1] - coords[7]) # Vertical distances
|
21 |
+
v2 = np.linalg.norm(coords[2] - coords[6])
|
22 |
+
v3 = np.linalg.norm(coords[3] - coords[5])
|
23 |
+
h1 = np.linalg.norm(coords[0] - coords[4]) # Horizontal distance
|
24 |
+
return (v1 + v2 + v3) / (2.0 * h1) if h1 > 0 else 0.0
|
25 |
+
|
26 |
+
class GeometricProcessor(BaseProcessor):
|
27 |
+
"""
|
28 |
+
Drowsiness detection using a combination of facial landmarks:
|
29 |
+
- Eye Aspect Ratio (EAR) for eye closure.
|
30 |
+
- Mouth Aspect Ratio (MAR) for yawning.
|
31 |
+
- Head Pose Estimation for nodding off or looking away.
|
32 |
+
"""
|
33 |
+
def __init__(self, config):
|
34 |
+
self.settings = config['geometric_settings']
|
35 |
+
self.face_mesh = mp.solutions.face_mesh.FaceMesh(
|
36 |
+
max_num_faces=1, refine_landmarks=True,
|
37 |
+
min_detection_confidence=0.5, min_tracking_confidence=0.5)
|
38 |
+
|
39 |
+
# State counters
|
40 |
+
self.counters = {
|
41 |
+
"eye_closure": 0, "yawning": 0,
|
42 |
+
"head_nod": 0, "looking_away": 0
|
43 |
+
}
|
44 |
+
|
45 |
+
# Landmark indices
|
46 |
+
self.L_EYE = [362, 385, 387, 263, 373, 380]
|
47 |
+
self.R_EYE = [33, 160, 158, 133, 153, 144]
|
48 |
+
self.MOUTH = [61, 291, 39, 181, 0, 17, 84, 178]
|
49 |
+
|
50 |
+
def process_frame(self, frame):
|
51 |
+
img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
52 |
+
h, w, _ = frame.shape
|
53 |
+
results = self.face_mesh.process(img_rgb)
|
54 |
+
|
55 |
+
drowsiness_indicators = {
|
56 |
+
"eye_closure": False, "yawning": False,
|
57 |
+
"head_nod": False, "looking_away": False, "details": {}
|
58 |
+
}
|
59 |
+
|
60 |
+
if results.multi_face_landmarks:
|
61 |
+
landmarks = results.multi_face_landmarks[0].landmark
|
62 |
+
|
63 |
+
# --- Eye Closure Detection (EAR) ---
|
64 |
+
left_ear = calculate_ear([landmarks[i] for i in self.L_EYE], (h, w))
|
65 |
+
right_ear = calculate_ear([landmarks[i] for i in self.R_EYE], (h, w))
|
66 |
+
ear = (left_ear + right_ear) / 2.0
|
67 |
+
if ear < self.settings['eye_ar_thresh']:
|
68 |
+
self.counters['eye_closure'] += 1
|
69 |
+
if self.counters['eye_closure'] >= self.settings['eye_ar_consec_frames']:
|
70 |
+
drowsiness_indicators['eye_closure'] = True
|
71 |
+
else:
|
72 |
+
self.counters['eye_closure'] = 0
|
73 |
+
drowsiness_indicators['details']['EAR'] = ear
|
74 |
+
|
75 |
+
# --- Yawn Detection (MAR) ---
|
76 |
+
mar = calculate_mar([landmarks[i] for i in self.MOUTH], (h, w))
|
77 |
+
if mar > self.settings['yawn_mar_thresh']:
|
78 |
+
self.counters['yawning'] += 1
|
79 |
+
if self.counters['yawning'] >= self.settings['yawn_consec_frames']:
|
80 |
+
drowsiness_indicators['yawning'] = True
|
81 |
+
else:
|
82 |
+
self.counters['yawning'] = 0
|
83 |
+
drowsiness_indicators['details']['MAR'] = mar
|
84 |
+
|
85 |
+
# --- Head Pose Estimation ---
|
86 |
+
face_3d = np.array([
|
87 |
+
[0.0, 0.0, 0.0], # Nose tip
|
88 |
+
[0.0, -330.0, -65.0], # Chin
|
89 |
+
[-225.0, 170.0, -135.0], # Left eye left corner
|
90 |
+
[225.0, 170.0, -135.0], # Right eye right corner
|
91 |
+
[-150.0, -150.0, -125.0], # Left Mouth corner
|
92 |
+
[150.0, -150.0, -125.0] # Right mouth corner
|
93 |
+
], dtype=np.float64)
|
94 |
+
face_2d = np.array([
|
95 |
+
(landmarks[1].x * w, landmarks[1].y * h), # Nose tip
|
96 |
+
(landmarks[152].x * w, landmarks[152].y * h), # Chin
|
97 |
+
(landmarks[263].x * w, landmarks[263].y * h), # Left eye corner
|
98 |
+
(landmarks[33].x * w, landmarks[33].y * h), # Right eye corner
|
99 |
+
(landmarks[287].x * w, landmarks[287].y * h), # Left mouth corner
|
100 |
+
(landmarks[57].x * w, landmarks[57].y * h) # Right mouth corner
|
101 |
+
], dtype=np.float64)
|
102 |
+
|
103 |
+
cam_matrix = np.array([[w, 0, w / 2], [0, w, h / 2], [0, 0, 1]], dtype=np.float64)
|
104 |
+
_, rot_vec, _ = cv2.solvePnP(face_3d, face_2d, cam_matrix, np.zeros((4, 1), dtype=np.float64))
|
105 |
+
rmat, _ = cv2.Rodrigues(rot_vec)
|
106 |
+
angles, _, _, _, _, _ = cv2.RQDecomp3x3(rmat)
|
107 |
+
|
108 |
+
pitch, yaw = angles[0], angles[1]
|
109 |
+
drowsiness_indicators['details']['Pitch'] = pitch
|
110 |
+
drowsiness_indicators['details']['Yaw'] = yaw
|
111 |
+
|
112 |
+
if pitch > self.settings['head_nod_thresh']:
|
113 |
+
self.counters['head_nod'] += 1
|
114 |
+
if self.counters['head_nod'] >= self.settings['head_pose_consec_frames']:
|
115 |
+
drowsiness_indicators['head_nod'] = True
|
116 |
+
else:
|
117 |
+
self.counters['head_nod'] = 0
|
118 |
+
|
119 |
+
if abs(yaw) > self.settings['head_look_away_thresh']:
|
120 |
+
self.counters['looking_away'] += 1
|
121 |
+
if self.counters['looking_away'] >= self.settings['head_pose_consec_frames']:
|
122 |
+
drowsiness_indicators['looking_away'] = True
|
123 |
+
else:
|
124 |
+
self.counters['looking_away'] = 0
|
125 |
+
|
126 |
+
# This processor now returns the frame and a dictionary of indicators
|
127 |
+
return frame, drowsiness_indicators
|
src/detection/strategies/hybrid.py
ADDED
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# drive_paddy/detection/strategies/hybrid.py
|
2 |
+
from src.detection.base_processor import BaseProcessor
|
3 |
+
from src.detection.strategies.geometric import GeometricProcessor
|
4 |
+
from src.detection.strategies.cnn_model import CnnProcessor
|
5 |
+
import cv2
|
6 |
+
import concurrent.futures
|
7 |
+
|
8 |
+
class HybridProcessor(BaseProcessor):
|
9 |
+
"""
|
10 |
+
Combines outputs from multiple detection strategies (Geometric and CNN)
|
11 |
+
concurrently to make a more robust and efficient drowsiness decision.
|
12 |
+
This version includes frame skipping for the CNN model to improve performance.
|
13 |
+
"""
|
14 |
+
def __init__(self, config):
|
15 |
+
self.geometric_processor = GeometricProcessor(config)
|
16 |
+
self.cnn_processor = CnnProcessor(config)
|
17 |
+
self.weights = config['hybrid_settings']['weights']
|
18 |
+
self.alert_threshold = config['hybrid_settings']['alert_threshold']
|
19 |
+
self.active_alerts = {}
|
20 |
+
|
21 |
+
# --- Performance Optimization ---
|
22 |
+
self.frame_counter = 0
|
23 |
+
self.cnn_process_interval = 10 # Run CNN every 10 frames
|
24 |
+
self.last_cnn_indicators = {"cnn_prediction": False} # Cache the last CNN result
|
25 |
+
|
26 |
+
self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=2)
|
27 |
+
|
28 |
+
def process_frame(self, frame):
|
29 |
+
self.frame_counter += 1
|
30 |
+
|
31 |
+
# --- Concurrent Execution ---
|
32 |
+
# The geometric processor runs on every frame.
|
33 |
+
geo_future = self.executor.submit(self.geometric_processor.process_frame, frame.copy())
|
34 |
+
|
35 |
+
# The CNN processor only runs on specified intervals.
|
36 |
+
if self.frame_counter % self.cnn_process_interval == 0:
|
37 |
+
cnn_future = self.executor.submit(self.cnn_processor.process_frame, frame.copy())
|
38 |
+
|
39 |
+
# Get the result from the geometric processor.
|
40 |
+
geo_frame, geo_indicators = geo_future.result()
|
41 |
+
|
42 |
+
# Get the CNN result if it was run, otherwise use the cached result.
|
43 |
+
if self.frame_counter % self.cnn_process_interval == 0:
|
44 |
+
_, self.last_cnn_indicators = cnn_future.result()
|
45 |
+
|
46 |
+
cnn_indicators = self.last_cnn_indicators
|
47 |
+
|
48 |
+
# Calculate weighted drowsiness score from the combined results.
|
49 |
+
score = 0
|
50 |
+
self.active_alerts.clear()
|
51 |
+
|
52 |
+
if geo_indicators.get("eye_closure"):
|
53 |
+
score += self.weights['eye_closure']
|
54 |
+
self.active_alerts['Eyes Closed'] = geo_indicators['details'].get('EAR', 0)
|
55 |
+
if geo_indicators.get("yawning"):
|
56 |
+
score += self.weights['yawning']
|
57 |
+
self.active_alerts['Yawning'] = geo_indicators['details'].get('MAR', 0)
|
58 |
+
if geo_indicators.get("head_nod"):
|
59 |
+
score += self.weights['head_nod']
|
60 |
+
self.active_alerts['Head Nod'] = geo_indicators['details'].get('Pitch', 0)
|
61 |
+
if geo_indicators.get("looking_away"):
|
62 |
+
score += self.weights['looking_away']
|
63 |
+
self.active_alerts['Looking Away'] = geo_indicators['details'].get('Yaw', 0)
|
64 |
+
if cnn_indicators.get("cnn_prediction"):
|
65 |
+
score += self.weights['cnn_prediction']
|
66 |
+
self.active_alerts['CNN Alert'] = 'Active'
|
67 |
+
|
68 |
+
# --- Visualization ---
|
69 |
+
output_frame = geo_frame
|
70 |
+
y_pos = 30
|
71 |
+
for alert, value in self.active_alerts.items():
|
72 |
+
text = f"{alert}: {value:.2f}" if isinstance(value, float) else alert
|
73 |
+
cv2.putText(output_frame, text, (10, y_pos), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2)
|
74 |
+
y_pos += 25
|
75 |
+
|
76 |
+
cv2.putText(output_frame, f"Score: {score:.2f}", (output_frame.shape[1] - 150, 30),
|
77 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
|
78 |
+
|
79 |
+
alert_triggered = score >= self.alert_threshold
|
80 |
+
if alert_triggered:
|
81 |
+
cv2.rectangle(output_frame, (0, 0), (output_frame.shape[1], output_frame.shape[0]), (0, 0, 255), 5)
|
82 |
+
|
83 |
+
# Return the processed frame, the alert trigger, and the active alert details
|
84 |
+
return output_frame, alert_triggered, self.active_alerts
|
src/streamlit_app.py
DELETED
@@ -1,40 +0,0 @@
|
|
1 |
-
import altair as alt
|
2 |
-
import numpy as np
|
3 |
-
import pandas as pd
|
4 |
-
import streamlit as st
|
5 |
-
|
6 |
-
"""
|
7 |
-
# Welcome to Streamlit!
|
8 |
-
|
9 |
-
Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
|
10 |
-
If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
|
11 |
-
forums](https://discuss.streamlit.io).
|
12 |
-
|
13 |
-
In the meantime, below is an example of what you can do with just a few lines of code:
|
14 |
-
"""
|
15 |
-
|
16 |
-
num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
|
17 |
-
num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
|
18 |
-
|
19 |
-
indices = np.linspace(0, 1, num_points)
|
20 |
-
theta = 2 * np.pi * num_turns * indices
|
21 |
-
radius = indices
|
22 |
-
|
23 |
-
x = radius * np.cos(theta)
|
24 |
-
y = radius * np.sin(theta)
|
25 |
-
|
26 |
-
df = pd.DataFrame({
|
27 |
-
"x": x,
|
28 |
-
"y": y,
|
29 |
-
"idx": indices,
|
30 |
-
"rand": np.random.randn(num_points),
|
31 |
-
})
|
32 |
-
|
33 |
-
st.altair_chart(alt.Chart(df, height=700, width=700)
|
34 |
-
.mark_point(filled=True)
|
35 |
-
.encode(
|
36 |
-
x=alt.X("x", axis=None),
|
37 |
-
y=alt.Y("y", axis=None),
|
38 |
-
color=alt.Color("idx", legend=None, scale=alt.Scale()),
|
39 |
-
size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
|
40 |
-
))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils.py
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# utils.py
|
2 |
+
|
3 |
+
import numpy as np
|
4 |
+
import cv2
|
5 |
+
# Removed: import random, string, generate_gibberish
|
6 |
+
|
7 |
+
# Function to calculate Eye Aspect Ratio (EAR)
|
8 |
+
def calculate_ear(eye_landmarks, frame_shape):
|
9 |
+
"""
|
10 |
+
Calculates the Eye Aspect Ratio (EAR) for a given eye.
|
11 |
+
|
12 |
+
Args:
|
13 |
+
eye_landmarks: A list of 6 MediaPipe landmark objects for the eye.
|
14 |
+
Expected order: [p1, p2, p3, p4, p5, p6]
|
15 |
+
where p1, p4 are horizontal extremes, and p2, p3, p5, p6
|
16 |
+
are vertical extremes.
|
17 |
+
frame_shape: Tuple (height, width) of the frame.
|
18 |
+
|
19 |
+
Returns:
|
20 |
+
The calculated EAR value.
|
21 |
+
"""
|
22 |
+
if len(eye_landmarks) != 6:
|
23 |
+
# print("Warning: Expected 6 eye landmarks, but received", len(eye_landmarks)) # Optional warning
|
24 |
+
return 0.0 # Return 0 or handle error appropriately
|
25 |
+
|
26 |
+
# Convert MediaPipe landmarks to numpy array (pixel coordinates)
|
27 |
+
coords = np.array([(landmark.x * frame_shape[1], landmark.y * frame_shape[0])
|
28 |
+
for landmark in eye_landmarks])
|
29 |
+
|
30 |
+
# Calculate the Euclidean distances between the two sets of vertical eye landmarks
|
31 |
+
# p2-p6 and p3-p5
|
32 |
+
vertical_dist1 = np.linalg.norm(coords[1] - coords[5])
|
33 |
+
vertical_dist2 = np.linalg.norm(coords[2] - coords[4])
|
34 |
+
|
35 |
+
# Calculate the Euclidean distance between the horizontal eye landmark
|
36 |
+
# p1-p4
|
37 |
+
horizontal_dist = np.linalg.norm(coords[0] - coords[3])
|
38 |
+
|
39 |
+
# Calculate the EAR
|
40 |
+
# Avoid division by zero
|
41 |
+
if horizontal_dist == 0:
|
42 |
+
return 0.0
|
43 |
+
|
44 |
+
ear = (vertical_dist1 + vertical_dist2) / (2.0 * horizontal_dist)
|
45 |
+
|
46 |
+
return ear
|
47 |
+
|
48 |
+
def draw_landmarks(image, landmarks, connections=None, point_color=(0, 255, 0), connection_color=(255, 255, 255)):
|
49 |
+
"""
|
50 |
+
Draws landmarks and connections on the image.
|
51 |
+
|
52 |
+
Args:
|
53 |
+
image: The image (numpy array) to draw on.
|
54 |
+
landmarks: A list of MediaPipe landmark objects.
|
55 |
+
connections: A list of tuples representing landmark connections (e.g., [(0, 1), (1, 2)]).
|
56 |
+
point_color: Color for the landmarks (BGR tuple).
|
57 |
+
connection_color: Color for the connections (BGR tuple).
|
58 |
+
"""
|
59 |
+
if not landmarks:
|
60 |
+
return image
|
61 |
+
|
62 |
+
img_h, img_w, _ = image.shape
|
63 |
+
landmark_points = [(int(l.x * img_w), int(l.y * img_h)) for l in landmarks]
|
64 |
+
|
65 |
+
# Draw connections
|
66 |
+
if connections:
|
67 |
+
for connection in connections:
|
68 |
+
p1 = landmark_points[connection[0]]
|
69 |
+
p2 = landmark_points[connection[1]]
|
70 |
+
cv2.line(image, p1, p2, connection_color, 1)
|
71 |
+
|
72 |
+
# Draw points
|
73 |
+
for point in landmark_points:
|
74 |
+
cv2.circle(image, point, 2, point_color, -1)
|
75 |
+
|
76 |
+
return image
|