Skip to content

Commit 43932a4

Browse files
committed
Files added for Drowsiness Detection post
1 parent cf923de commit 43932a4

File tree

12 files changed

+1059
-0
lines changed

12 files changed

+1059
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/.vscode
2+
__pycache__

Driver-Drowsiness-detection-using-Mediapipe-in-Python/Blog-Code-Demo.ipynb

+628
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Guide to Driver Drowsines Detection using Mediapipe in Python
2+
3+
**This repository contains code for blog post: [Guide to Driver Drowsines Detection using Mediapipe in Python](https://learnopencv.com/guide-to-driver-drowsiness-detection-using-mediapipe-in-python/) blog post**.
4+
5+
In this post, we will:
6+
7+
* Learn how to detect eye landmarks using the Mediapipe Face Mesh solution pipeline in python.
8+
* Introduce and demonstrate the Eye Aspect Ratio (EAR) technique.
9+
* Create a Drowsiness Detection web application using streamlit.
10+
* Use streamlit-webrtc to help transmit real-time video/audio streams over the network.
11+
* Deploy it on a cloud service.
12+
13+
<img src = 'app_image/page_SS.jpg'>
14+
15+
# AI Courses by OpenCV
16+
17+
Want to become an expert in AI? [AI Courses by OpenCV](https://opencv.org/courses/) is a great place to start.
18+
19+
<a href="https://opencv.org/courses/">
20+
<p align="center">
21+
<img src="https://www.learnopencv.com/wp-content/uploads/2020/04/AI-Courses-By-OpenCV-Github.png">
22+
</p>
23+
</a>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
css_string = """
2+
<style>
3+
4+
.sidebar {max-width:100%; float:right}
5+
.side-block img {width:100%}
6+
.side-block {margin-bottom:15px}
7+
.side-block a.button {margin-bottom:30px; background: #006CFF; color: #fff;font-weight: 500;font-size: 16px;line-height: 20px;padding: 15px;display: inline-block;max-width: 300px;border-radius: 5px; text-decoration:none; }
8+
.side-block a.button:hover {background:#000}
9+
10+
</style>
11+
12+
<div class="sidebar">
13+
14+
<div class="side-block">
15+
<a target="_blank" href="https://opencv.org/courses" rel="noopener">
16+
<img src="https://learnopencv.com/wp-content/uploads/2022/03/opencv-course1.png" alt="Opencv Courses">
17+
</div>
18+
<div class="side-block">
19+
<a href="https://learnopencv.com" class="button ">Subscribe To My Newsletter</a>
20+
<div class="side-block">
21+
</div>
22+
<a target="_blank" href="https://pallet.xyz/list/ai-jobs?" rel="noopener">
23+
<img src="https://learnopencv.com/wp-content/uploads/2022/02/learnopencv-ai-jobs.jpg" alt="Opencv Courses">
24+
</div>
25+
<div class="side-block">
26+
<a target="_blank" href="https://bigvision.ai" rel="noopener">
27+
<img src="https://learnopencv.com/wp-content/uploads/2022/02/bigvision.jpg" alt="Opencv Courses"></a>
28+
</div>
29+
</div>
30+
31+
"""
Loading
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import av
2+
import numpy as np
3+
from pydub import AudioSegment
4+
5+
6+
class AudioFrameHandler:
7+
"""To play/pass custom audio based on some event"""
8+
9+
def __init__(self, sound_file_path: str = ""):
10+
11+
self.custom_audio = AudioSegment.from_file(file=sound_file_path, format="wav")
12+
self.custom_audio_len = len(self.custom_audio)
13+
14+
self.ms_per_audio_segment: int = 20
15+
self.audio_segment_shape: tuple
16+
17+
self.play_state_tracker: dict = {"curr_segment": -1} # Currently playing segment
18+
self.audio_segments_created: bool = False
19+
self.audio_segments: list = []
20+
21+
def prepare_audio(self, frame: av.AudioFrame):
22+
raw_samples = frame.to_ndarray()
23+
sound = AudioSegment(
24+
data=raw_samples.tobytes(),
25+
sample_width=frame.format.bytes,
26+
frame_rate=frame.sample_rate,
27+
channels=len(frame.layout.channels),
28+
)
29+
30+
self.ms_per_audio_segment = len(sound)
31+
self.audio_segment_shape = raw_samples.shape
32+
33+
self.custom_audio = self.custom_audio.set_channels(sound.channels)
34+
self.custom_audio = self.custom_audio.set_frame_rate(sound.frame_rate)
35+
self.custom_audio = self.custom_audio.set_sample_width(sound.sample_width)
36+
37+
self.audio_segments = [
38+
self.custom_audio[i : i + self.ms_per_audio_segment]
39+
for i in range(0, self.custom_audio_len - self.custom_audio_len % self.ms_per_audio_segment, self.ms_per_audio_segment)
40+
]
41+
self.total_segments = len(self.audio_segments) - 1 # -1 because we start from 0.
42+
43+
self.audio_segments_created = True
44+
45+
def process(self, frame: av.AudioFrame, play_sound: bool = False):
46+
47+
"""
48+
Takes in the current input audio frame and based on play_sound boolean value
49+
either starts sending the custom audio frame or dampens the frame wave to emulate silence.
50+
51+
For eg. playing a notification based on some event.
52+
"""
53+
54+
if not self.audio_segments_created:
55+
self.prepare_audio(frame)
56+
57+
raw_samples = frame.to_ndarray()
58+
_curr_segment = self.play_state_tracker["curr_segment"]
59+
60+
if play_sound:
61+
if _curr_segment < self.total_segments:
62+
_curr_segment += 1
63+
else:
64+
_curr_segment = 0
65+
66+
sound = self.audio_segments[_curr_segment]
67+
68+
else:
69+
if -1 < _curr_segment < self.total_segments:
70+
_curr_segment += 1
71+
sound = self.audio_segments[_curr_segment]
72+
else:
73+
_curr_segment = -1
74+
sound = AudioSegment(
75+
data=raw_samples.tobytes(),
76+
sample_width=frame.format.bytes,
77+
frame_rate=frame.sample_rate,
78+
channels=len(frame.layout.channels),
79+
)
80+
sound = sound.apply_gain(-100)
81+
82+
self.play_state_tracker["curr_segment"] = _curr_segment
83+
84+
channel_sounds = sound.split_to_mono()
85+
channel_samples = [s.get_array_of_samples() for s in channel_sounds]
86+
87+
new_samples = np.array(channel_samples).T
88+
89+
new_samples = new_samples.reshape(self.audio_segment_shape)
90+
new_frame = av.AudioFrame.from_ndarray(new_samples, layout=frame.layout.name)
91+
new_frame.sample_rate = frame.sample_rate
92+
93+
return new_frame
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import cv2
2+
import time
3+
import numpy as np
4+
import mediapipe as mp
5+
from mediapipe.python.solutions.drawing_utils import _normalized_to_pixel_coordinates as denormalize_coordinates
6+
7+
8+
def get_mediapipe_app(
9+
max_num_faces=1,
10+
refine_landmarks=True,
11+
min_detection_confidence=0.5,
12+
min_tracking_confidence=0.5,
13+
):
14+
"""Initialize and return Mediapipe FaceMesh Solution Graph object"""
15+
face_mesh = mp.solutions.face_mesh.FaceMesh(
16+
max_num_faces=max_num_faces,
17+
refine_landmarks=refine_landmarks,
18+
min_detection_confidence=min_detection_confidence,
19+
min_tracking_confidence=min_tracking_confidence,
20+
)
21+
22+
return face_mesh
23+
24+
25+
def distance(point_1, point_2):
26+
"""Calculate l2-norm between two points"""
27+
dist = sum([(i - j) ** 2 for i, j in zip(point_1, point_2)]) ** 0.5
28+
return dist
29+
30+
31+
def get_ear(landmarks, refer_idxs, frame_width, frame_height):
32+
"""
33+
Calculate Eye Aspect Ratio for one eye.
34+
35+
Args:
36+
landmarks: (list) Detected landmarks list
37+
refer_idxs: (list) Index positions of the chosen landmarks
38+
in order P1, P2, P3, P4, P5, P6
39+
frame_width: (int) Width of captured frame
40+
frame_height: (int) Height of captured frame
41+
42+
Returns:
43+
ear: (float) Eye aspect ratio
44+
"""
45+
try:
46+
# Compute the euclidean distance between the horizontal
47+
coords_points = []
48+
for i in refer_idxs:
49+
lm = landmarks[i]
50+
coord = denormalize_coordinates(lm.x, lm.y, frame_width, frame_height)
51+
coords_points.append(coord)
52+
53+
# Eye landmark (x, y)-coordinates
54+
P2_P6 = distance(coords_points[1], coords_points[5])
55+
P3_P5 = distance(coords_points[2], coords_points[4])
56+
P1_P4 = distance(coords_points[0], coords_points[3])
57+
58+
# Compute the eye aspect ratio
59+
ear = (P2_P6 + P3_P5) / (2.0 * P1_P4)
60+
61+
except:
62+
ear = 0.0
63+
coords_points = None
64+
65+
return ear, coords_points
66+
67+
68+
def calculate_avg_ear(landmarks, left_eye_idxs, right_eye_idxs, image_w, image_h):
69+
# Calculate Eye aspect ratio
70+
71+
left_ear, left_lm_coordinates = get_ear(landmarks, left_eye_idxs, image_w, image_h)
72+
right_ear, right_lm_coordinates = get_ear(landmarks, right_eye_idxs, image_w, image_h)
73+
Avg_EAR = (left_ear + right_ear) / 2.0
74+
75+
return Avg_EAR, (left_lm_coordinates, right_lm_coordinates)
76+
77+
78+
def plot_eye_landmarks(frame, left_lm_coordinates, right_lm_coordinates, color):
79+
for lm_coordinates in [left_lm_coordinates, right_lm_coordinates]:
80+
if lm_coordinates:
81+
for coord in lm_coordinates:
82+
cv2.circle(frame, coord, 2, color, -1)
83+
84+
frame = cv2.flip(frame, 1)
85+
return frame
86+
87+
88+
def plot_text(image, text, origin, color, font=cv2.FONT_HERSHEY_SIMPLEX, fntScale=0.8, thickness=2):
89+
image = cv2.putText(image, text, origin, font, fntScale, color, thickness)
90+
return image
91+
92+
93+
class VideoFrameHandler:
94+
def __init__(self):
95+
"""
96+
Initialize the necessary constants, mediapipe app
97+
and tracker variables
98+
"""
99+
# Left and right eye chosen landmarks.
100+
self.eye_idxs = {
101+
"left": [362, 385, 387, 263, 373, 380],
102+
"right": [33, 160, 158, 133, 153, 144],
103+
}
104+
105+
# Used for coloring landmark points.
106+
# Its value depends on the current EAR value.
107+
self.RED = (0, 0, 255) # BGR
108+
self.GREEN = (0, 255, 0) # BGR
109+
110+
# Initializing Mediapipe FaceMesh solution pipeline
111+
self.facemesh_model = get_mediapipe_app()
112+
113+
# For tracking counters and sharing states in and out of callbacks.
114+
self.state_tracker = {
115+
"start_time": time.perf_counter(),
116+
"DROWSY_TIME": 0.0, # Holds the amount of time passed with EAR < EAR_THRESH
117+
"COLOR": self.GREEN,
118+
"play_alarm": False,
119+
}
120+
121+
self.EAR_txt_pos = (10, 30)
122+
123+
def process(self, frame: np.array, thresholds: dict):
124+
"""
125+
This function is used to implement our Drowsy detection algorithm
126+
127+
Args:
128+
frame: (np.array) Input frame matrix.
129+
thresholds: (dict) Contains the two threshold values
130+
WAIT_TIME and EAR_THRESH.
131+
132+
Returns:
133+
The processed frame and a boolean flag to
134+
indicate if the alarm should be played or not.
135+
"""
136+
137+
# To improve performance,
138+
# mark the frame as not writeable to pass by reference.
139+
frame.flags.writeable = False
140+
frame_h, frame_w, _ = frame.shape
141+
142+
DROWSY_TIME_txt_pos = (10, int(frame_h // 2 * 1.7))
143+
ALM_txt_pos = (10, int(frame_h // 2 * 1.85))
144+
145+
results = self.facemesh_model.process(frame)
146+
147+
if results.multi_face_landmarks:
148+
landmarks = results.multi_face_landmarks[0].landmark
149+
EAR, coordinates = calculate_avg_ear(landmarks, self.eye_idxs["left"], self.eye_idxs["right"], frame_w, frame_h)
150+
frame = plot_eye_landmarks(frame, coordinates[0], coordinates[1], self.state_tracker["COLOR"])
151+
152+
if EAR < thresholds["EAR_THRESH"]:
153+
154+
# Increase DROWSY_TIME to track the time period with EAR less than the threshold
155+
# and reset the start_time for the next iteration.
156+
end_time = time.perf_counter()
157+
158+
self.state_tracker["DROWSY_TIME"] += end_time - self.state_tracker["start_time"]
159+
self.state_tracker["start_time"] = end_time
160+
self.state_tracker["COLOR"] = self.RED
161+
162+
if self.state_tracker["DROWSY_TIME"] >= thresholds["WAIT_TIME"]:
163+
self.state_tracker["play_alarm"] = True
164+
plot_text(frame, "WAKE UP! WAKE UP", ALM_txt_pos, self.state_tracker["COLOR"])
165+
166+
else:
167+
self.state_tracker["start_time"] = time.perf_counter()
168+
self.state_tracker["DROWSY_TIME"] = 0.0
169+
self.state_tracker["COLOR"] = self.GREEN
170+
self.state_tracker["play_alarm"] = False
171+
172+
EAR_txt = f"EAR: {round(EAR, 2)}"
173+
DROWSY_TIME_txt = f"DROWSY: {round(self.state_tracker['DROWSY_TIME'], 3)} Secs"
174+
plot_text(frame, EAR_txt, self.EAR_txt_pos, self.state_tracker["COLOR"])
175+
plot_text(frame, DROWSY_TIME_txt, DROWSY_TIME_txt_pos, self.state_tracker["COLOR"])
176+
177+
else:
178+
self.state_tracker["start_time"] = time.perf_counter()
179+
self.state_tracker["DROWSY_TIME"] = 0.0
180+
self.state_tracker["COLOR"] = self.GREEN
181+
self.state_tracker["play_alarm"] = False
182+
183+
# Flip the frame horizontally for a selfie-view display.
184+
frame = cv2.flip(frame, 1)
185+
186+
return frame, self.state_tracker["play_alarm"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
cryptography==38.0.0
2+
pyOpenSSL
3+
aiortc
4+
numpy
5+
pydub
6+
mediapipe
7+
streamlit==1.13.0
8+
streamlit_webrtc==0.43.4
9+
streamlit-nested-layout==0.1.1

0 commit comments

Comments
 (0)