Testimony Adekoya commited on
Commit
19f420a
·
1 Parent(s): bd08a17

Work on drive-paddy to huggingface

Browse files
.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
- *.7z filter=lfs diff=lfs merge=lfs -text
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: Streamlit template space
12
  ---
13
 
14
- # Welcome to Streamlit!
15
 
16
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- altair
2
- pandas
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