samir1120 commited on
Commit
0b80e89
·
verified ·
1 Parent(s): fcd2d07

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +664 -37
src/streamlit_app.py CHANGED
@@ -1,40 +1,667 @@
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
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import mediapipe as mp
3
  import numpy as np
4
+ import logging
5
+ import time
6
+ from filterpy.kalman import KalmanFilter
7
+ import pyttsx3
8
+ import threading
9
  import streamlit as st
10
+ from streamlit_webrtc import webrtc_streamer, VideoProcessorBase, RTCConfiguration
11
 
12
+ # Suppress MediaPipe warnings
13
+ logging.getLogger('mediapipe').setLevel(logging.ERROR)
14
+
15
+ # Initialize MediaPipe Pose
16
+ mp_pose = mp.solutions.pose
17
+ mp_drawing = mp.solutions.drawing_utils
18
+
19
+ pose = mp_pose.Pose(
20
+ static_image_mode=False,
21
+ min_detection_confidence=0.5,
22
+ min_tracking_confidence=0.6,
23
+ model_complexity=2,
24
+ smooth_landmarks=True
25
+ )
26
+
27
+ # Initialize pyttsx3 for text-to-speech
28
+ engine = pyttsx3.init()
29
+ engine.setProperty('rate', 150) # Speed of speech
30
+ engine.setProperty('volume', 0.9) # Volume (0.0 to 1.0)
31
+
32
+ # Function to speak text in a separate thread to avoid blocking
33
+ def speak(text, force=False):
34
+ if not hasattr(speak, 'last_text') or speak.last_text != text or force:
35
+ speak.last_text = text
36
+ def run_speech():
37
+ engine.say(text)
38
+ engine.runAndWait()
39
+ threading.Thread(target=run_speech, daemon=True).start()
40
+
41
+ # Define scaling factor for angles
42
+ ANGLE_SCALE = 1
43
+
44
+ # Initialize Kalman Filter for smoothing angles
45
+ def initialize_kalman_filter():
46
+ kf = KalmanFilter(dim_x=6, dim_z=3)
47
+ kf.x = np.zeros(6)
48
+ kf.F = np.array([
49
+ [1, 0, 0, 1, 0, 0],
50
+ [0, 1, 0, 0, 1, 0],
51
+ [0, 0, 1, 0, 0, 1],
52
+ [0, 0, 0, 1, 0, 0],
53
+ [0, 0, 0, 0, 1, 0],
54
+ [0, 0, 0, 0, 0, 1]
55
+ ])
56
+ kf.H = np.array([
57
+ [1, 0, 0, 0, 0, 0],
58
+ [0, 1, 0, 0, 0, 0],
59
+ [0, 0, 1, 0, 0, 0]
60
+ ])
61
+ kf.P *= 10.
62
+ kf.R = np.diag([1.0, 1.0, 1.0])
63
+ kf.Q = np.eye(6) * 0.05
64
+ return kf
65
+
66
+ kf = initialize_kalman_filter()
67
+
68
+ # Load target pose (same as original)
69
+ target_pose = [
70
+ {
71
+ "person_id": 0,
72
+ "bbox": [
73
+ 260.447998046875,
74
+ 434.9598693847656,
75
+ 263.357177734375,
76
+ 439.172119140625
77
+ ],
78
+ "keypoints": [
79
+ {"name": "Nose", "x": 240.35791015625, "y": 135.41705322265625, "score": 0.9791688919067383},
80
+ {"name": "L_Eye", "x": 265.16717529296875, "y": 110.43780517578125, "score": 0.9833072428857386},
81
+ {"name": "R_Eye", "x": 210.517822265625, "y": 114.45855712890625, "score": 0.9687361121177673},
82
+ {"name": "L_Ear", "x": 301.84814453125, "y": 135.83111572265625, "score": 0.9493670302238464},
83
+ {"name": "R_Ear", "x": 175.035888671875, "y": 143.1534423828125, "score": 0.9537781476974487},
84
+ {"name": "L_Shoulder", "x": 367.36688232421875, "y": 277.89508056640625, "score": 0.9714463949203491},
85
+ {"name": "R_Shoulder", "x": 132.6015625, "y": 287.1273193359375, "score": 0.9208009243011475},
86
+ {"name": "L_Elbow", "x": 404.8804931640625, "y": 457.8016357421875, "score": 1.0068358182907104},
87
+ {"name": "R_Elbow", "x": 121.6767578125, "y": 466.985595703125, "score": 0.9445005059242249},
88
+ {"name": "L_Wrist", "x": 316.5948486328125, "y": 564.1590576171875, "score": 0.9202994108200073},
89
+ {"name": "R_Wrist", "x": 218.354248046875, "y": 578.4954833984375, "score": 0.9106894731521606},
90
+ {"name": "L_Hip", "x": 343.258056640625, "y": 562.5377197265625, "score": 0.8454821705818176},
91
+ {"name": "R_Hip", "x": 191.992431640625, "y": 569.1612548828125, "score": 0.856957733631134},
92
+ {"name": "L_Knee", "x": 394.12591552734375, "y": 672.401611328125, "score": 0.8698152899742126},
93
+ {"name": "R_Knee", "x": 143.781005859375, "y": 696.0062255859375, "score": 0.8501293659210205},
94
+ {"name": "L_Ankle", "x": 353.07330322265625, "y": 853.671142578125, "score": 0.9136713147163391},
95
+ {"name": "R_Ankle", "x": 211.80206298828125, "y": 850.3348388671875, "score": 0.8354711532592773}
96
+ ]
97
+ }
98
+ ]
99
+
100
+ # Extract and center target keypoints
101
+ frame_width = 1280
102
+ frame_height = 720
103
+ target_keypoints = [(kp["x"], kp["y"]) for kp in target_pose[0]["keypoints"]]
104
+ head_keypoint_indices = [0, 1, 2, 3, 4]
105
+ head_keypoints = [target_keypoints[i] for i in head_keypoint_indices]
106
+ target_head_center_x = sum(x for x, y in head_keypoints) / len(head_keypoints)
107
+ target_head_center_y = sum(y for x, y in head_keypoints) / len(head_keypoints)
108
+ display_center_x = frame_width / 2
109
+ display_center_y = frame_height * 0.2
110
+ translate_x = display_center_x - target_head_center_x
111
+ translate_y = display_center_y - target_head_center_y
112
+ centered_target_keypoints = [(x + translate_x, y + translate_y) for x, y in target_keypoints]
113
+ head_keypoints_centered = [centered_target_keypoints[i] for i in head_keypoint_indices]
114
+ x_coords = [x for x, y in head_keypoints_centered]
115
+ y_coords = [y for x, y in head_keypoints_centered]
116
+ bbox_min_x = max(0, min(x_coords) - 20)
117
+ bbox_max_x = min(frame_width, max(x_coords) + 20)
118
+ bbox_min_y = max(0, min(y_coords) - 20)
119
+ bbox_max_y = min(frame_height, max(y_coords) + 20)
120
+
121
+ # Helper functions (same as original)
122
+ def euclidean_distance(p1, p2):
123
+ return np.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)
124
+
125
+ def is_head_pose_matched(user_landmarks, target_keypoints, distance_threshold=25):
126
+ head_indices_mapping = {0: 0, 2: 1, 5: 2, 7: 3, 8: 4}
127
+ for mp_idx, target_idx in head_indices_mapping.items():
128
+ if mp_idx < len(user_landmarks) and target_idx < len(target_keypoints):
129
+ distance = euclidean_distance(user_landmarks[mp_idx], target_keypoints[target_idx])
130
+ if distance > distance_threshold:
131
+ return False
132
+ return True
133
+
134
+ def is_full_body_visible(landmarks, frame_width, frame_height):
135
+ key_landmarks = [
136
+ mp_pose.PoseLandmark.LEFT_SHOULDER,
137
+ mp_pose.PoseLandmark.RIGHT_SHOULDER,
138
+ mp_pose.PoseLandmark.LEFT_HIP,
139
+ mp_pose.PoseLandmark.RIGHT_HIP,
140
+ ]
141
+ for landmark in key_landmarks:
142
+ lm = landmarks[landmark]
143
+ if (lm.visibility < 0.6 or
144
+ lm.x < 0.05 or lm.x > 0.95 or
145
+ lm.y < 0.05 or lm.y > 0.95):
146
+ return False
147
+ return True
148
+
149
+ def _calculate_raw_head_angles_user_method(landmark_list):
150
+ required_indices = [mp_pose.PoseLandmark.NOSE, mp_pose.PoseLandmark.LEFT_EAR, mp_pose.PoseLandmark.RIGHT_EAR,
151
+ mp_pose.PoseLandmark.LEFT_EYE, mp_pose.PoseLandmark.RIGHT_EYE]
152
+ if landmark_list is None or len(landmark_list) <= max(idx.value for idx in required_indices):
153
+ return None
154
+ for l_idx_enum in required_indices:
155
+ if landmark_list[l_idx_enum.value].visibility < 0.5:
156
+ return None
157
+ nose = landmark_list[mp_pose.PoseLandmark.NOSE.value]
158
+ left_ear = landmark_list[mp_pose.PoseLandmark.LEFT_EAR.value]
159
+ right_ear = landmark_list[mp_pose.PoseLandmark.RIGHT_EAR.value]
160
+ left_eye = landmark_list[mp_pose.PoseLandmark.LEFT_EYE.value]
161
+ right_eye = landmark_list[mp_pose.PoseLandmark.RIGHT_EYE.value]
162
+ mid_ear = np.array([(left_ear.x + right_ear.x) / 2,
163
+ (left_ear.y + right_ear.y) / 2,
164
+ (left_ear.z + right_ear.z) / 2])
165
+ nose_vec = mid_ear - np.array([nose.x, nose.y, nose.z])
166
+ yaw = -np.degrees(np.arctan2(nose_vec[0], nose_vec[2] + 1e-6))
167
+ eye_mid = np.array([(left_eye.x + right_eye.x) / 2,
168
+ (left_eye.y + right_eye.y) / 2,
169
+ (left_eye.z + right_eye.z) / 2])
170
+ nose_to_eye = np.array([nose.x, nose.y, nose.z]) - eye_mid
171
+ pitch = np.degrees(np.arctan2(nose_to_eye[1], np.sqrt(nose_to_eye[0]**2 + nose_to_eye[2]**2 + 1e-6)))
172
+ ear_vec_2d = np.array([left_ear.x - right_ear.x, left_ear.y - right_ear.y])
173
+ roll = np.degrees(np.arctan2(ear_vec_2d[1], ear_vec_2d[0] + 1e-6))
174
+ return yaw, -(pitch - 50), roll
175
+
176
+ def get_head_angles(pose_results):
177
+ raw_yaw, raw_pitch, raw_roll = 0.0, 0.0, 0.0
178
+ if pose_results and pose_results.pose_landmarks:
179
+ try:
180
+ angles = _calculate_raw_head_angles_user_method(
181
+ pose_results.pose_landmarks.landmark
182
+ )
183
+ if angles is not None:
184
+ raw_yaw, raw_pitch, raw_roll = angles
185
+ except Exception as e:
186
+ logging.error(f"Error in get_head_angles: {e}")
187
+ kf.predict()
188
+ kf.update(np.array([raw_yaw, raw_pitch, raw_roll]))
189
+ smoothed_yaw, smoothed_pitch, smoothed_roll = kf.x[:3]
190
+ return smoothed_yaw * ANGLE_SCALE * 3, smoothed_pitch * ANGLE_SCALE, smoothed_roll * ANGLE_SCALE
191
+
192
+ def wrap_angle_180(angle):
193
+ wrapped_angle = np.fmod(angle + 180, 360)
194
+ if wrapped_angle < 0:
195
+ wrapped_angle += 360
196
+ return wrapped_angle - 180
197
+
198
+ # VideoProcessor for streamlit-webrtc
199
+ class VideoProcessor(VideoProcessorBase):
200
+ def __init__(self):
201
+ self.visibility_confirmed = False
202
+ self.match_start_time = None
203
+ self.match_duration_threshold = 5
204
+ self.pose_held = False
205
+ self.bppv_step_1 = False
206
+ self.bppv_step_2 = False
207
+ self.bppv_step_3 = False
208
+ self.bppv_step_4 = False
209
+ self.bppv_start_time = None
210
+ self.bppv_duration_threshold = 30
211
+ self.neutral_hold_threshold = 5
212
+ self.bppv_pose_held_time = 0
213
+ self.mission_complete = False
214
+ self.step_3_complete = False
215
+ self.all_missions_complete = False
216
+ self.last_speech_time = 0
217
+ self.speech_interval = 3
218
+ self.in_correct_pose_step_1 = False
219
+ self.in_correct_pose_step_2 = False
220
+ self.in_correct_pose_step_3 = False
221
+ self.in_correct_pose_step_4 = False
222
+ self.head_shake_count = 0
223
+ self.head_shake_complete = False
224
+ self.last_yaw = 0
225
+ self.yaw_direction = 0
226
+ self.yaw_threshold = 15
227
+ self.target_yaw_min_step_1 = 25
228
+ self.target_yaw_max_step_1 = 65
229
+ self.target_yaw_min_step_2 = -20
230
+ self.target_yaw_max_step_2 = 20
231
+ self.target_pitch_min_step_2 = 70
232
+ self.target_pitch_max_step_2 = 110
233
+ self.target_roll_min_step_2 = -120
234
+ self.target_roll_max_step_2 = -80
235
+ self.target_yaw_min_step_3 = 153
236
+ self.target_yaw_max_step_3 = 193
237
+ self.target_pitch_min_step_3 = 17
238
+ self.target_pitch_max_step_3 = 57
239
+ self.target_roll_min_step_3 = 77
240
+ self.target_roll_max_step_3 = 117
241
+ self.target_yaw_min_step_4 = -160
242
+ self.target_yaw_max_step_4 = 160
243
+ self.target_pitch_min_step_4 = 0
244
+ self.target_pitch_max_step_4 = 30
245
+ self.target_roll_min_step_4 = -160
246
+ self.target_roll_max_step_4 = 160
247
+
248
+ def recv(self, frame):
249
+ img = frame.to_ndarray(format="bgr24")
250
+ img = cv2.flip(img, 1)
251
+ img = cv2.resize(img, (frame_width, frame_height))
252
+ image_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
253
+ pose_results = pose.process(image_rgb)
254
+ img = cv2.cvtColor(image_rgb, cv2.COLOR_RGB2BGR)
255
+
256
+ current_time = time.time()
257
+
258
+ if pose_results.pose_landmarks:
259
+ landmarks = pose_results.pose_landmarks.landmark
260
+ user_landmarks = [(lm.x * frame_width, lm.y * frame_height) for lm in landmarks]
261
+
262
+ # Stage 1: Full-body visibility check
263
+ if not self.visibility_confirmed:
264
+ if is_full_body_visible(landmarks, frame_width, frame_height):
265
+ self.visibility_confirmed = True
266
+ cv2.putText(img, "Visibility Confirmed!", (frame_width // 4, frame_height // 2 - 50),
267
+ cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2, cv2.LINE_AA)
268
+ if current_time - self.last_speech_time > self.speech_interval:
269
+ speak("Full body visibility confirmed. Please adjust your head to match the position that your eye and nose point are fully inside the box and box should be green", force=True)
270
+ self.last_speech_time = current_time
271
+ else:
272
+ cv2.putText(img, "Please move back for full body visibility", (frame_width // 4 - 50, frame_height // 2),
273
+ cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 3, cv2.LINE_AA)
274
+ if current_time - self.last_speech_time > self.speech_interval:
275
+ speak("Please move back to ensure your full body is visible in the frame.")
276
+ self.last_speech_time = current_time
277
+ self.match_start_time = None
278
+ self.pose_held = False
279
+ self.bppv_step_1 = False
280
+ self.bppv_step_2 = False
281
+ self.bppv_step_3 = False
282
+ self.bppv_step_4 = False
283
+ self.mission_complete = False
284
+ self.step_3_complete = False
285
+ self.all_missions_complete = False
286
+ self.in_correct_pose_step_1 = False
287
+ self.in_correct_pose_step_2 = False
288
+ self.in_correct_pose_step_3 = False
289
+ self.in_correct_pose_step_4 = False
290
+ self.head_shake_count = 0
291
+ self.head_shake_complete = False
292
+ self.yaw_direction = 0
293
+
294
+ # Stage 2: Head pose matching and calibration
295
+ elif self.visibility_confirmed and not self.pose_held:
296
+ head_pose_matched = is_head_pose_matched(user_landmarks, centered_target_keypoints)
297
+ bbox_color = (0, 255, 0) if head_pose_matched else (0, 0, 255)
298
+ cv2.rectangle(img, (int(bbox_min_x), int(bbox_min_y)), (int(bbox_max_x), int(bbox_max_y)),
299
+ bbox_color, 2)
300
+
301
+ if head_pose_matched:
302
+ if self.match_start_time is None:
303
+ self.match_start_time = current_time
304
+ if current_time - self.last_speech_time > self.speech_interval:
305
+ speak("Hold your head in this position.")
306
+ self.last_speech_time = current_time
307
+ else:
308
+ elapsed_time = current_time - self.match_start_time
309
+ if elapsed_time >= self.match_duration_threshold:
310
+ self.pose_held = True
311
+ self.bppv_step_1 = True
312
+ speak("Calibration complete. Now turn your head 45 degrees to the right and hold for 30 seconds.", force=True)
313
+ self.last_speech_time = current_time
314
+ self.bppv_start_time = current_time
315
+ else:
316
+ remaining_time = max(0, self.match_duration_threshold - elapsed_time)
317
+ cv2.putText(img, f"Hold Head Pose for {remaining_time:.1f}s",
318
+ (frame_width // 4, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
319
+ else:
320
+ self.match_start_time = None
321
+ if current_time - self.last_speech_time > self.speech_interval:
322
+ speak("Adjust your head to make the box green for 5 seconds.", force=True)
323
+ self.last_speech_time = current_time
324
+ cv2.putText(img, "Adjust eye and nose in the centre of box", (frame_width // 4, 50),
325
+ cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
326
+
327
+ # Stage 3: BPPV Step 1
328
+ elif self.pose_held and self.bppv_step_1 and not self.mission_complete:
329
+ current_head_yaw, current_head_pitch, current_head_roll = get_head_angles(pose_results)
330
+ display_yaw = wrap_angle_180(current_head_yaw)
331
+ display_pitch = wrap_angle_180(current_head_pitch)
332
+ display_roll = wrap_angle_180(current_head_roll)
333
+
334
+ yaw_correct = self.target_yaw_min_step_1 <= display_yaw <= self.target_yaw_max_step_1
335
+
336
+ if yaw_correct:
337
+ if not self.in_correct_pose_step_1:
338
+ speak("Hold this position for 30 seconds.", force=True)
339
+ self.last_speech_time = current_time
340
+ self.in_correct_pose_step_1 = True
341
+ if self.bppv_start_time is None:
342
+ self.bppv_start_time = current_time
343
+ self.bppv_pose_held_time = current_time - self.bppv_start_time
344
+ remaining_time = max(0, self.bppv_duration_threshold - self.bppv_pose_held_time)
345
+ cv2.putText(img, f"Hold Head at this position for {remaining_time:.1f}s",
346
+ (frame_width // 4, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
347
+ if current_time - self.last_speech_time > self.speech_interval:
348
+ speak(f"Hold your head at this position {remaining_time:.1f} seconds remaining.")
349
+ self.last_speech_time = current_time
350
+ if self.bppv_pose_held_time >= self.bppv_duration_threshold:
351
+ self.mission_complete = True
352
+ self.bppv_step_2 = True
353
+ speak("Step 1 complete. Now, slowly lie down on your left side, so that your right ear rests on the bed.Keep your head aligned—same position as before.Hold this pose for 30 seconds and stay relaxed.", force=True)
354
+ self.last_speech_time = current_time
355
+ self.bppv_start_time = None
356
+ self.bppv_pose_held_time = 0
357
+ self.in_correct_pose_step_1 = False
358
+ else:
359
+ self.bppv_start_time = None
360
+ self.in_correct_pose_step_1 = False
361
+ if display_yaw < self.target_yaw_min_step_1:
362
+ if current_time - self.last_speech_time > self.speech_interval:
363
+ speak("Turn your head further to the right.", force=True)
364
+ self.last_speech_time = current_time
365
+ cv2.putText(img, "Turn head further right", (frame_width // 4, 50),
366
+ cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
367
+ elif display_yaw > self.target_yaw_max_step_1:
368
+ if current_time - self.last_speech_time > self.speech_interval:
369
+ speak("Turn your head back to the left.", force=True)
370
+ self.last_speech_time = current_time
371
+ cv2.putText(img, "Turn head back left", (frame_width // 4, 50),
372
+ cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
373
+
374
+ cv2.putText(img, f"Yaw: {int(display_yaw)}", (10, 100), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
375
+ cv2.putText(img, f"Pitch: {int(display_pitch)}", (10, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
376
+ cv2.putText(img, f"Roll: {int(display_roll)}", (10, 200), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
377
+
378
+ # Stage 4: BPPV Step 2
379
+ elif self.mission_complete and self.bppv_step_2 and not self.step_3_complete:
380
+ current_head_yaw, current_head_pitch, current_head_roll = get_head_angles(pose_results)
381
+ display_yaw = wrap_angle_180(current_head_yaw)
382
+ display_pitch = wrap_angle_180(current_head_pitch)
383
+ display_roll = wrap_angle_180(current_head_roll)
384
+
385
+ yaw_correct = self.target_yaw_min_step_2 <= display_yaw <= self.target_yaw_max_step_2
386
+ pitch_correct = self.target_pitch_min_step_2 <= display_pitch <= self.target_pitch_max_step_2
387
+ roll_correct = self.target_roll_min_step_2 <= display_roll <= self.target_roll_max_step_2
388
+ pose_correct = yaw_correct and pitch_correct and roll_correct
389
+
390
+ if pose_correct:
391
+ if not self.in_correct_pose_step_2:
392
+ speak("Hold this position for 30 seconds.", force=True)
393
+ self.last_speech_time = current_time
394
+ self.in_correct_pose_step_2 = True
395
+ if self.bppv_start_time is None:
396
+ self.bppv_start_time = current_time
397
+ self.bppv_pose_held_time = current_time - self.bppv_start_time
398
+ remaining_time = max(0, self.bppv_duration_threshold - self.bppv_pose_held_time)
399
+ cv2.putText(img, f"Hold Head at this position for {remaining_time:.1f}s",
400
+ (frame_width // 4, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
401
+ if current_time - self.last_speech_time > self.speech_interval:
402
+ speak(f"Hold your head in this position. {remaining_time:.1f} seconds remaining.")
403
+ self.last_speech_time = current_time
404
+ if self.bppv_pose_held_time >= self.bppv_duration_threshold:
405
+ self.step_3_complete = True
406
+ self.bppv_step_3 = True
407
+ speak("Step 2 complete. stay you head at the same angle, and roll your body to right and hold for 30 seconds.", force=True)
408
+ self.last_speech_time = current_time
409
+ self.bppv_start_time = None
410
+ self.bppv_pose_held_time = 0
411
+ self.in_correct_pose_step_2 = False
412
+ else:
413
+ self.bppv_start_time = None
414
+ self.in_correct_pose_step_2 = False
415
+ error_messages = []
416
+ if not yaw_correct:
417
+ if display_yaw < self.target_yaw_min_step_2:
418
+ error_messages.append("Turn your head to the left.")
419
+ if current_time - self.last_speech_time > self.speech_interval:
420
+ speak("Turn your head to the left.", force=True)
421
+ self.last_speech_time = current_time
422
+ elif display_yaw > self.target_yaw_max_step_2:
423
+ error_messages.append("Turn your head to the right.")
424
+ if current_time - self.last_speech_time > self.speech_interval:
425
+ speak("Turn your head to the right.", force=True)
426
+ self.last_speech_time = current_time
427
+ if not pitch_correct:
428
+ if display_pitch < self.target_pitch_min_step_2:
429
+ error_messages.append("Tilt your head further up.")
430
+ if current_time - self.last_speech_time > self.speech_interval:
431
+ speak("Tilt your head further up.", force=True)
432
+ self.last_speech_time = current_time
433
+ elif display_pitch > self.target_pitch_max_step_2:
434
+ error_messages.append("Tilt your head down.")
435
+ if current_time - self.last_speech_time > self.speech_interval:
436
+ speak("Tilt your head down.", force=True)
437
+ self.last_speech_time = current_time
438
+ if not roll_correct:
439
+ if display_roll < self.target_roll_min_step_2:
440
+ error_messages.append("bend your head more to the left.")
441
+ if current_time - self.last_speech_time > self.speech_interval:
442
+ speak("bend your head more to the left.", force=True)
443
+ self.last_speech_time = current_time
444
+ elif display_roll > self.target_roll_max_step_2:
445
+ error_messages.append("bend your head to the right.")
446
+ if current_time - self.last_speech_time > self.speech_interval:
447
+ speak("bend your head to the right.", force=True)
448
+ self.last_speech_time = current_time
449
+ error_text = " ".join(error_messages) if error_messages else "Adjust head to target pose."
450
+ cv2.putText(img, error_text, (frame_width // 4, 50),
451
+ cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
452
+
453
+ cv2.putText(img, f"Yaw: {int(display_yaw)}", (10, 100), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
454
+ cv2.putText(img, f"Pitch: {int(display_pitch)}", (10, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
455
+ cv2.putText(img, f"Roll: {int(display_roll)}", (10, 200), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
456
+
457
+ # Stage 5: BPPV Step 3
458
+ elif self.step_3_complete and self.bppv_step_3 and not self.bppv_step_4:
459
+ current_head_yaw, current_head_pitch, current_head_roll = get_head_angles(pose_results)
460
+ display_yaw = wrap_angle_180(current_head_yaw)
461
+ display_pitch = wrap_angle_180(current_head_pitch)
462
+ display_roll = wrap_angle_180(current_head_roll)
463
+
464
+ yaw_correct = self.target_yaw_min_step_3 <= display_yaw <= self.target_yaw_max_step_3
465
+ pitch_correct = self.target_pitch_min_step_3 <= display_pitch <= self.target_pitch_max_step_3
466
+ roll_correct = self.target_roll_min_step_3 <= display_roll <= self.target_roll_max_step_3
467
+ pose_correct = yaw_correct and pitch_correct and roll_correct
468
+
469
+ if pose_correct:
470
+ if not self.in_correct_pose_step_3:
471
+ speak("Hold this position for 30 seconds.", force=True)
472
+ self.last_speech_time = current_time
473
+ self.in_correct_pose_step_3 = True
474
+ if self.bppv_start_time is None:
475
+ self.bppv_start_time = current_time
476
+ self.bppv_pose_held_time = current_time - self.bppv_start_time
477
+ remaining_time = max(0, self.bppv_duration_threshold - self.bppv_pose_held_time)
478
+ cv2.putText(img, f"Hold Head at this position for {remaining_time:.1f}s",
479
+ (frame_width // 4, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
480
+ if current_time - self.last_speech_time > self.speech_interval:
481
+ speak(f"Hold your head in this position. {remaining_time:.1f} seconds remaining.")
482
+ self.last_speech_time = current_time
483
+ if self.bppv_pose_held_time >= self.bppv_duration_threshold:
484
+ self.bppv_step_4 = True
485
+ speak("Step 3 complete. Now shake your head side to side 2 to 3 times, then sit on the opposite side of the bed in a neutral position.", force=True)
486
+ self.last_speech_time = current_time
487
+ self.bppv_start_time = None
488
+ self.bppv_pose_held_time = 0
489
+ self.in_correct_pose_step_3 = False
490
+ self.last_yaw = display_yaw
491
+ else:
492
+ self.bppv_start_time = None
493
+ self.in_correct_pose_step_3 = False
494
+ error_messages = []
495
+ if not yaw_correct:
496
+ if display_yaw < self.target_yaw_min_step_3:
497
+ error_messages.append("Turn your head further to the left.")
498
+ if current_time - self.last_speech_time > self.speech_interval:
499
+ speak("Turn your head further to the left.", force=True)
500
+ self.last_speech_time = current_time
501
+ elif display_yaw > self.target_yaw_max_step_3:
502
+ error_messages.append("Turn your head back to the right.")
503
+ if current_time - self.last_speech_time > self.speech_interval:
504
+ speak("Turn your head back to the right.", force=True)
505
+ self.last_speech_time = current_time
506
+ if not pitch_correct:
507
+ if display_pitch < self.target_pitch_min_step_3:
508
+ error_messages.append("Tilt your head further up.")
509
+ if current_time - self.last_speech_time > self.speech_interval:
510
+ speak("Tilt your head further up.", force=True)
511
+ self.last_speech_time = current_time
512
+ elif display_pitch > self.target_pitch_max_step_3:
513
+ error_messages.append("Tilt your head down.")
514
+ if current_time - self.last_speech_time > self.speech_interval:
515
+ speak("Tilt your head down.", force=True)
516
+ self.last_speech_time = current_time
517
+ if not roll_correct:
518
+ if display_roll < self.target_roll_min_step_3:
519
+ error_messages.append("bend your head more to the right.")
520
+ if current_time - self.last_speech_time > self.speech_interval:
521
+ speak("bend your head more to the right.", force=True)
522
+ self.last_speech_time = current_time
523
+ elif display_roll > self.target_roll_max_step_3:
524
+ error_messages.append("bend your head to the left.")
525
+ if current_time - self.last_speech_time > self.speech_interval:
526
+ speak("bend your head to the left.", force=True)
527
+ self.last_speech_time = current_time
528
+ error_text = " ".join(error_messages) if error_messages else "Adjust head to target pose."
529
+ cv2.putText(img, error_text, (frame_width // 4, 50),
530
+ cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
531
+
532
+ cv2.putText(img, f"Yaw: {int(display_yaw)}", (10, 100), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
533
+ cv2.putText(img, f"Pitch: {int(display_pitch)}", (10, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
534
+ cv2.putText(img, f"Roll: {int(display_roll)}", (10, 200), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
535
+
536
+ # Stage 6: BPPV Step 4
537
+ elif self.bppv_step_4 and not self.all_missions_complete:
538
+ current_head_yaw, current_head_pitch, current_head_roll = get_head_angles(pose_results)
539
+ display_yaw = wrap_angle_180(current_head_yaw)
540
+ display_pitch = wrap_angle_180(current_head_pitch)
541
+ display_roll = wrap_angle_180(current_head_roll)
542
+
543
+ if not self.head_shake_complete:
544
+ yaw_change = display_yaw - self.last_yaw
545
+ if yaw_change > self.yaw_threshold and self.yaw_direction != 1:
546
+ self.yaw_direction = 1
547
+ self.head_shake_count += 0.5
548
+ elif yaw_change < -self.yaw_threshold and self.yaw_direction != -1:
549
+ self.yaw_direction = -1
550
+ self.head_shake_count += 0.5
551
+
552
+ self.last_yaw = display_yaw
553
+
554
+ if self.head_shake_count < 2:
555
+ cv2.putText(img, f"Shake head side to side ({int(self.head_shake_count*2)}/2-3 shakes)",
556
+ (frame_width // 4, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
557
+ if current_time - self.last_speech_time > self.speech_interval:
558
+ speak("Keep shaking your head side to side.", force=True)
559
+ self.last_speech_time = current_time
560
+ else:
561
+ self.head_shake_complete = True
562
+ speak("Now sit on the opposite side of the bed in a neutral position.", force=True)
563
+ self.last_speech_time = current_time
564
+ self.bppv_start_time = None
565
+ self.in_correct_pose_step_4 = False
566
+
567
+ else:
568
+ yaw_correct = (display_yaw < self.target_yaw_min_step_4) or (display_yaw > self.target_yaw_max_step_4)
569
+ pitch_correct = (self.target_pitch_min_step_4 <= display_pitch <= self.target_pitch_max_step_4)
570
+ roll_correct = (display_roll < self.target_roll_min_step_4) or (display_roll > self.target_roll_max_step_4)
571
+ pose_correct = yaw_correct and pitch_correct and roll_correct
572
+
573
+ if pose_correct:
574
+ if not self.in_correct_pose_step_4:
575
+ speak("Hold this neutral position for 30 seconds.", force=True)
576
+ self.last_speech_time = current_time
577
+ self.in_correct_pose_step_4 = True
578
+ if self.bppv_start_time is None:
579
+ self.bppv_start_time = current_time
580
+ self.bppv_pose_held_time = current_time - self.bppv_start_time
581
+ remaining_time = max(0, self.neutral_hold_threshold - self.bppv_pose_held_time)
582
+ cv2.putText(img, f"Hold Neutral Position for {remaining_time:.1f}s",
583
+ (frame_width // 4, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
584
+ if current_time - self.last_speech_time > self.speech_interval:
585
+ speak(f"Hold this neutral position. {remaining_time:.1f} seconds remaining.")
586
+ self.last_speech_time = current_time
587
+ if self.bppv_pose_held_time >= self.neutral_hold_threshold:
588
+ self.all_missions_complete = True
589
+ speak("You have successfully completed the maneuver.", force=True)
590
+ self.last_speech_time = current_time
591
+ else:
592
+ self.bppv_start_time = None
593
+ self.in_correct_pose_step_4 = False
594
+ error_messages = []
595
+ if not yaw_correct:
596
+ if display_yaw >= 0:
597
+ error_messages.append("Turn your head further right.")
598
+ if current_time - self.last_speech_time > self.speech_interval:
599
+ speak("Turn your head further right.", force=True)
600
+ self.last_speech_time = current_time
601
+ else:
602
+ error_messages.append("Turn your head further left.")
603
+ if current_time - self.last_speech_time > self.speech_interval:
604
+ speak("Turn your head further left.", force=True)
605
+ self.last_speech_time = current_time
606
+ if not pitch_correct:
607
+ if display_pitch < self.target_pitch_min_step_4:
608
+ error_messages.append("Tilt your head further up.")
609
+ if current_time - self.last_speech_time > self.speech_interval:
610
+ speak("Tilt your head further up.", force=True)
611
+ self.last_speech_time = current_time
612
+ elif display_pitch > self.target_pitch_max_step_4:
613
+ error_messages.append("Tilt your head down.")
614
+ if current_time - self.last_speech_time > self.speech_interval:
615
+ speak("Tilt your head down.", force=True)
616
+ self.last_speech_time = current_time
617
+ if not roll_correct:
618
+ if display_roll < self.target_roll_min_step_4:
619
+ error_messages.append("bend your head to the right.")
620
+ if current_time - self.last_speech_time > self.speech_interval:
621
+ speak("bend your head to the right.", force=True)
622
+ self.last_speech_time = current_time
623
+ elif display_roll > self.target_roll_max_step_4:
624
+ error_messages.append("bend your head to the left.")
625
+ if current_time - self.last_speech_time > self.speech_interval:
626
+ speak("bend your head to the left.", force=True)
627
+ self.last_speech_time = current_time
628
+ error_text = " ".join(error_messages) if error_messages else "Adjust to neutral position."
629
+ cv2.putText(img, error_text, (frame_width // 4, 50),
630
+ cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
631
+
632
+ cv2.putText(img, f"Yaw: {int(display_yaw)}", (10, 100), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
633
+ cv2.putText(img, f"Pitch: {int(display_pitch)}", (10, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
634
+ cv2.putText(img, f"Roll: {int(display_roll)}", (10, 200), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
635
+
636
+ # Stage 7: All Missions Complete
637
+ elif self.all_missions_complete:
638
+ cv2.putText(img, "Epley Maneuver Guider Complete!", (frame_width // 4, frame_height // 2),
639
+ cv2.FONT_HERSHEY_SIMPLEX, 1.5, (0, 255, 0), 3, cv2.LINE_AA)
640
+ current_head_yaw, current_head_pitch, current_head_roll = get_head_angles(pose_results)
641
+ display_yaw = wrap_angle_180(current_head_yaw)
642
+ display_pitch = wrap_angle_180(current_head_pitch)
643
+ display_roll = wrap_angle_180(current_head_roll)
644
+ cv2.putText(img, f"Yaw: {int(display_yaw)}", (10, 100), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
645
+ cv2.putText(img, f"Pitch: {int(display_pitch)}", (10, 150), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
646
+ cv2.putText(img, f"Roll: {int(display_roll)}", (10, 200), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2)
647
+
648
+ mp_drawing.draw_landmarks(img, pose_results.pose_landmarks, mp_pose.POSE_CONNECTIONS,
649
+ landmark_drawing_spec=mp_drawing.DrawingSpec(color=(255, 0, 0), thickness=2, circle_radius=4),
650
+ connection_drawing_spec=mp_drawing.DrawingSpec(color=(255, 0, 0), thickness=2))
651
+
652
+ return img
653
+
654
+ def main():
655
+ st.title("AI Based BPPV Maneuver Guider")
656
+ st.write("Ensure your webcam is enabled and follow the instructions to perform the Epley Maneuver.")
657
+
658
+ # WebRTC streamer configuration
659
+ webrtc_streamer(
660
+ key="bppv-guider",
661
+ video_processor_factory=VideoProcessor,
662
+ rtc_configuration=RTCConfiguration({"iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]}),
663
+ media_stream_constraints={"video": True, "audio": False},
664
+ )
665
+
666
+ if __name__ == "__main__":
667
+ main()