veb-101 commited on
Commit
10181aa
·
1 Parent(s): 82fe2d1

first HF push

Browse files
Files changed (8) hide show
  1. .gitignore +4 -0
  2. README.md +14 -1
  3. ads.py +31 -0
  4. app.py +87 -0
  5. audio/wake_up.wav +0 -0
  6. audio_handling.py +93 -0
  7. drowsy_detection.py +187 -0
  8. requirements.txt +10 -0
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ /.vscode
2
+ __pycache__
3
+ drowsiness-detection-course
4
+ main_2.py
README.md CHANGED
@@ -10,4 +10,17 @@ pinned: false
10
  license: afl-3.0
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  license: afl-3.0
11
  ---
12
 
13
+ # Drowsiness-Detection-Using-Mediapipe-Streamlit
14
+
15
+ A drowsiness detection application created using mediapipe, streamlit and streamlit-webrtc
16
+
17
+ This repository contains the app deployment code on streamlit cloud. <br>
18
+ To understand more refer to my blogpost: [Driver Drowsiness Detection Using Mediapipe In Python | LearnOpenCV](https://learnopencv.com/driver-drowsiness-detection-using-mediapipe-in-python/)
19
+
20
+
21
+
22
+ Libraries used:
23
+
24
+ 1. Mediapipe face mesh: https://google.github.io/mediapipe/solutions/face_mesh.html
25
+ 2. Streamlit: https://streamlit.io/
26
+ 3. streamlit-webrtc: https://github.com/whitphx/streamlit-webrtc
ads.py ADDED
@@ -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
+ """
app.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import av
3
+ import threading
4
+ import streamlit as st
5
+ import streamlit_nested_layout
6
+ from streamlit_webrtc import VideoHTMLAttributes, webrtc_streamer
7
+
8
+ from audio_handling import AudioFrameHandler
9
+ from drowsy_detection import VideoFrameHandler
10
+ from ads import css_string
11
+
12
+
13
+ # Define the audio file to use.
14
+ alarm_file_path = os.path.join("audio", "wake_up.wav")
15
+
16
+ # Streamlit Components
17
+ st.set_page_config(
18
+ page_title="Drowsiness Detection | LearnOpenCV",
19
+ page_icon="https://learnopencv.com/wp-content/uploads/2017/12/favicon.png",
20
+ layout="wide", # centered, wide
21
+ initial_sidebar_state="expanded",
22
+ menu_items={
23
+ "About": "### Visit www.learnopencv.com for more exciting tutorials!!!",
24
+ },
25
+ )
26
+
27
+
28
+ col1, col2 = st.columns(spec=[6, 2], gap="medium")
29
+
30
+ with col1:
31
+ st.title("Drowsiness Detection!!!🥱😪😴")
32
+ with st.container():
33
+ c1, c2 = st.columns(spec=[1, 1])
34
+ with c1:
35
+ # The amount of time (in seconds) to wait before sounding the alarm.
36
+ WAIT_TIME = st.slider("Seconds to wait before sounding alarm:", 0.0, 5.0, 1.0, 0.25)
37
+
38
+ with c2:
39
+ # Lowest valid value of Eye Aspect Ratio. Ideal values [0.15, 0.2].
40
+ EAR_THRESH = st.slider("Eye Aspect Ratio threshold:", 0.0, 0.4, 0.18, 0.01)
41
+
42
+ thresholds = {
43
+ "EAR_THRESH": EAR_THRESH,
44
+ "WAIT_TIME": WAIT_TIME,
45
+ }
46
+
47
+ # For streamlit-webrtc
48
+ video_handler = VideoFrameHandler()
49
+ audio_handler = AudioFrameHandler(sound_file_path=alarm_file_path)
50
+
51
+ lock = threading.Lock() # For thread-safe access & to prevent race-condition.
52
+ shared_state = {"play_alarm": False}
53
+
54
+
55
+ def video_frame_callback(frame: av.VideoFrame):
56
+ frame = frame.to_ndarray(format="bgr24") # Decode and convert frame to RGB
57
+
58
+ frame, play_alarm = video_handler.process(frame, thresholds) # Process frame
59
+ with lock:
60
+ shared_state["play_alarm"] = play_alarm # Update shared state
61
+
62
+ return av.VideoFrame.from_ndarray(frame, format="bgr24") # Encode and return BGR frame
63
+
64
+
65
+ def audio_frame_callback(frame: av.AudioFrame):
66
+ with lock: # access the current “play_alarm” state
67
+ play_alarm = shared_state["play_alarm"]
68
+
69
+ new_frame: av.AudioFrame = audio_handler.process(frame, play_sound=play_alarm)
70
+ return new_frame
71
+
72
+
73
+ # https://github.com/whitphx/streamlit-webrtc/blob/main/streamlit_webrtc/config.py
74
+
75
+ with col1:
76
+ ctx = webrtc_streamer(
77
+ key="drowsiness-detection",
78
+ video_frame_callback=video_frame_callback,
79
+ audio_frame_callback=audio_frame_callback,
80
+ rtc_configuration={"iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]}, # Add this to config for cloud deployment.
81
+ media_stream_constraints={"video": {"height": {"ideal": 480}}, "audio": True},
82
+ video_html_attrs=VideoHTMLAttributes(autoPlay=True, controls=False, muted=False),
83
+ )
84
+
85
+ with col2:
86
+ # Banner for newsletter subscription, jobs, and consulting.
87
+ st.markdown(css_string, unsafe_allow_html=True)
audio/wake_up.wav ADDED
Binary file (28.9 kB). View file
 
audio_handling.py ADDED
@@ -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
drowsy_detection.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
40
+ frame_width: (int) Width of captured frame
41
+ frame_height: (int) Height of captured frame
42
+
43
+ Returns:
44
+ ear: (float) Eye aspect ratio
45
+ """
46
+ try:
47
+ # Compute the euclidean distance between the horizontal
48
+ coords_points = []
49
+ for i in refer_idxs:
50
+ lm = landmarks[i]
51
+ coord = denormalize_coordinates(lm.x, lm.y, frame_width, frame_height)
52
+ coords_points.append(coord)
53
+
54
+ # Eye landmark (x, y)-coordinates
55
+ P2_P6 = distance(coords_points[1], coords_points[5])
56
+ P3_P5 = distance(coords_points[2], coords_points[4])
57
+ P1_P4 = distance(coords_points[0], coords_points[3])
58
+
59
+ # Compute the eye aspect ratio
60
+ ear = (P2_P6 + P3_P5) / (2.0 * P1_P4)
61
+
62
+ except:
63
+ ear = 0.0
64
+ coords_points = None
65
+
66
+ return ear, coords_points
67
+
68
+
69
+ def calculate_avg_ear(landmarks, left_eye_idxs, right_eye_idxs, image_w, image_h):
70
+ # Calculate Eye aspect ratio
71
+
72
+ left_ear, left_lm_coordinates = get_ear(landmarks, left_eye_idxs, image_w, image_h)
73
+ right_ear, right_lm_coordinates = get_ear(landmarks, right_eye_idxs, image_w, image_h)
74
+ Avg_EAR = (left_ear + right_ear) / 2.0
75
+
76
+ return Avg_EAR, (left_lm_coordinates, right_lm_coordinates)
77
+
78
+
79
+ def plot_eye_landmarks(frame, left_lm_coordinates, right_lm_coordinates, color):
80
+ for lm_coordinates in [left_lm_coordinates, right_lm_coordinates]:
81
+ if lm_coordinates:
82
+ for coord in lm_coordinates:
83
+ cv2.circle(frame, coord, 2, color, -1)
84
+
85
+ frame = cv2.flip(frame, 1)
86
+ return frame
87
+
88
+
89
+ def plot_text(image, text, origin, color, font=cv2.FONT_HERSHEY_SIMPLEX, fntScale=0.8, thickness=2):
90
+ image = cv2.putText(image, text, origin, font, fntScale, color, thickness)
91
+ return image
92
+
93
+
94
+ class VideoFrameHandler:
95
+ def __init__(self):
96
+ """
97
+ Initialize the necessary constants, mediapipe app
98
+ and tracker variables
99
+ """
100
+ # Left and right eye chosen landmarks.
101
+ self.eye_idxs = {
102
+ "left": [362, 385, 387, 263, 373, 380],
103
+ "right": [33, 160, 158, 133, 153, 144],
104
+ }
105
+
106
+ # Used for coloring landmark points.
107
+ # Its value depends on the current EAR value.
108
+ self.RED = (0, 0, 255) # BGR
109
+ self.GREEN = (0, 255, 0) # BGR
110
+
111
+ # Initializing Mediapipe FaceMesh solution pipeline
112
+ self.facemesh_model = get_mediapipe_app()
113
+
114
+ # For tracking counters and sharing states in and out of callbacks.
115
+ self.state_tracker = {
116
+ "start_time": time.perf_counter(),
117
+ "DROWSY_TIME": 0.0, # Holds the amount of time passed with EAR < EAR_THRESH
118
+ "COLOR": self.GREEN,
119
+ "play_alarm": False,
120
+ }
121
+
122
+ self.EAR_txt_pos = (10, 30)
123
+
124
+ def process(self, frame: np.array, thresholds: dict):
125
+ """
126
+ This function is used to implement our Drowsy detection algorithm
127
+
128
+ Args:
129
+ frame: (np.array) Input frame matrix.
130
+ thresholds: (dict) Contains the two threshold values
131
+ WAIT_TIME and EAR_THRESH.
132
+
133
+ Returns:
134
+ The processed frame and a boolean flag to
135
+ indicate if the alarm should be played or not.
136
+ """
137
+
138
+ # To improve performance,
139
+ # mark the frame as not writeable to pass by reference.
140
+ frame.flags.writeable = False
141
+ frame_h, frame_w, _ = frame.shape
142
+
143
+ DROWSY_TIME_txt_pos = (10, int(frame_h // 2 * 1.7))
144
+ ALM_txt_pos = (10, int(frame_h // 2 * 1.85))
145
+
146
+ results = self.facemesh_model.process(frame)
147
+
148
+ if results.multi_face_landmarks:
149
+ landmarks = results.multi_face_landmarks[0].landmark
150
+ EAR, coordinates = calculate_avg_ear(landmarks, self.eye_idxs["left"], self.eye_idxs["right"], frame_w, frame_h)
151
+ frame = plot_eye_landmarks(frame, coordinates[0], coordinates[1], self.state_tracker["COLOR"])
152
+
153
+ if EAR < thresholds["EAR_THRESH"]:
154
+
155
+ # Increase DROWSY_TIME to track the time period with EAR less than threshold
156
+ # and reset the start_time for the next iteration.
157
+ end_time = time.perf_counter()
158
+
159
+ self.state_tracker["DROWSY_TIME"] += end_time - self.state_tracker["start_time"]
160
+ self.state_tracker["start_time"] = end_time
161
+ self.state_tracker["COLOR"] = self.RED
162
+
163
+ if self.state_tracker["DROWSY_TIME"] >= thresholds["WAIT_TIME"]:
164
+ self.state_tracker["play_alarm"] = True
165
+ plot_text(frame, "WAKE UP! WAKE UP", ALM_txt_pos, self.state_tracker["COLOR"])
166
+
167
+ else:
168
+ self.state_tracker["start_time"] = time.perf_counter()
169
+ self.state_tracker["DROWSY_TIME"] = 0.0
170
+ self.state_tracker["COLOR"] = self.GREEN
171
+ self.state_tracker["play_alarm"] = False
172
+
173
+ EAR_txt = f"EAR: {round(EAR, 2)}"
174
+ DROWSY_TIME_txt = f"DROWSY: {round(self.state_tracker['DROWSY_TIME'], 3)} Secs"
175
+ plot_text(frame, EAR_txt, self.EAR_txt_pos, self.state_tracker["COLOR"])
176
+ plot_text(frame, DROWSY_TIME_txt, DROWSY_TIME_txt_pos, self.state_tracker["COLOR"])
177
+
178
+ else:
179
+ self.state_tracker["start_time"] = time.perf_counter()
180
+ self.state_tracker["DROWSY_TIME"] = 0.0
181
+ self.state_tracker["COLOR"] = self.GREEN
182
+ self.state_tracker["play_alarm"] = False
183
+
184
+ # Flip the frame horizontally for a selfie-view display.
185
+ frame = cv2.flip(frame, 1)
186
+
187
+ return frame, self.state_tracker["play_alarm"]
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ cryptography
2
+ pyOpenSSL
3
+ aiortc
4
+ numpy
5
+ opencv-python-headless
6
+ pydub
7
+ mediapipe
8
+ streamlit
9
+ streamlit_webrtc
10
+ streamlit-nested-layout