zforkash commited on
Commit
a7c179a
·
verified ·
1 Parent(s): e25d2e1
Files changed (1) hide show
  1. app.py +720 -0
app.py CHANGED
@@ -0,0 +1,720 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Consolidated Streamlit App
2
+ import streamlit as st
3
+ import subprocess
4
+
5
+ # Title and introduction
6
+ st.title("Workout Tracker")
7
+ st.markdown("""
8
+ Welcome to the **Workout Tracker App**!
9
+ Select your desired workout below, and the app will guide you through the exercise with real-time feedback.
10
+ """)
11
+
12
+ # Workout options
13
+ st.header("Choose Your Workout")
14
+ workout_option = st.selectbox(
15
+ "Available Workouts:",
16
+ ["Bicep Curl", "Lateral Raise", "Shoulder Press"]
17
+ )
18
+
19
+ # Button to start the workout
20
+ if st.button("Start Workout"):
21
+ st.write(f"Starting {workout_option}...")
22
+
23
+ # Map the workout to the corresponding script
24
+ workout_scripts = {
25
+ "Bicep Curl": "bicep_curl.py",
26
+ "Lateral Raise": "lateral_raise.py",
27
+ "Shoulder Press": "shoulder_press.py",
28
+ }
29
+
30
+ selected_script = workout_scripts.get(workout_option)
31
+
32
+ # Run the corresponding script
33
+ try:
34
+ subprocess.run(["python", selected_script], check=True)
35
+ st.success(f"{workout_option} workout completed! Check the feedback on your terminal.")
36
+ except subprocess.CalledProcessError as e:
37
+ st.error(f"An error occurred while running {workout_option}. Please try again.")
38
+ except FileNotFoundError:
39
+ st.error(f"Workout script {selected_script} not found! Ensure the file exists in the same directory.")
40
+
41
+ # Footer
42
+ st.markdown("""
43
+ ---
44
+ **Note**: Close the workout window or press "q" in the camera feed to stop the workout.
45
+ """)
46
+
47
+
48
+ # From bicep_with_feedback.py
49
+ import cv2
50
+ import mediapipe as mp
51
+ import numpy as np
52
+ import time
53
+ from sklearn.ensemble import IsolationForest
54
+
55
+ # Mediapipe utilities
56
+ mp_drawing = mp.solutions.drawing_utils
57
+ mp_pose = mp.solutions.pose
58
+
59
+
60
+ # Function to calculate angles between three points
61
+ def calculate_angle(a, b, c):
62
+ a = np.array(a)
63
+ b = np.array(b)
64
+ c = np.array(c)
65
+
66
+ radians = np.arctan2(c[1] - b[1], c[0] - b[0]) - np.arctan2(a[1] - b[1], a[0] - b[0])
67
+ angle = np.abs(np.degrees(radians))
68
+ if angle > 180.0:
69
+ angle = 360 - angle
70
+ return angle
71
+
72
+
73
+ # Function to draw text with a background
74
+ def draw_text_with_background(image, text, position, font, font_scale, color, thickness, bg_color, padding=10):
75
+ text_size = cv2.getTextSize(text, font, font_scale, thickness)[0]
76
+ text_x, text_y = position
77
+ box_coords = (
78
+ (text_x - padding, text_y - padding),
79
+ (text_x + text_size[0] + padding, text_y + text_size[1] + padding),
80
+ )
81
+ cv2.rectangle(image, box_coords[0], box_coords[1], bg_color, cv2.FILLED)
82
+ cv2.putText(image, text, (text_x, text_y + text_size[1]), font, font_scale, color, thickness)
83
+
84
+
85
+ # Real-time feedback for single rep
86
+ def analyze_single_rep(rep, rep_data):
87
+ """Provide actionable feedback for a single rep."""
88
+ feedback = []
89
+ avg_rom = np.mean([r["ROM"] for r in rep_data])
90
+ avg_tempo = np.mean([r["Tempo"] for r in rep_data])
91
+ avg_smoothness = np.mean([r["Smoothness"] for r in rep_data])
92
+
93
+ if rep["ROM"] < avg_rom * 0.8:
94
+ feedback.append("Extend arm more")
95
+ if rep["Tempo"] < avg_tempo * 0.8:
96
+ feedback.append("Slow down")
97
+ if rep["Smoothness"] > avg_smoothness * 1.2:
98
+ feedback.append("Move smoothly")
99
+
100
+ return " | ".join(feedback) if feedback else "Good rep!"
101
+
102
+
103
+ # Post-workout feedback function with Isolation Forest
104
+ def analyze_workout_with_isolation_forest(rep_data):
105
+ if not rep_data:
106
+ print("No reps completed.")
107
+ return
108
+
109
+ print("\n--- Post-Workout Summary ---")
110
+
111
+ # Convert rep_data to a feature matrix
112
+ features = np.array([[rep["ROM"], rep["Tempo"], rep["Smoothness"]] for rep in rep_data])
113
+
114
+ # Train Isolation Forest
115
+ model = IsolationForest(contamination=0.2, random_state=42)
116
+ predictions = model.fit_predict(features)
117
+
118
+ # Analyze reps
119
+ for i, (rep, prediction) in enumerate(zip(rep_data, predictions), 1):
120
+ status = "Good" if prediction == 1 else "Anomalous"
121
+ reason = []
122
+ if prediction == -1: # If anomalous
123
+ if rep["ROM"] < np.mean(features[:, 0]) - np.std(features[:, 0]):
124
+ reason.append("Low ROM")
125
+ if rep["Tempo"] < np.mean(features[:, 1]) - np.std(features[:, 1]):
126
+ reason.append("Too Fast")
127
+ if rep["Smoothness"] > np.mean(features[:, 2]) + np.std(features[:, 2]):
128
+ reason.append("Jerky Movement")
129
+ reason_str = ", ".join(reason) if reason else "None"
130
+ print(f"Rep {i}: {status} | ROM: {rep['ROM']:.2f}, Tempo: {rep['Tempo']:.2f}s, Smoothness: {rep['Smoothness']:.2f} | Reason: {reason_str}")
131
+
132
+
133
+ # Main workout tracking function
134
+ def main():
135
+ cap = cv2.VideoCapture(0)
136
+ counter = 0 # Rep counter
137
+ stage = None # Movement stage
138
+ max_reps = 10
139
+ rep_data = [] # Store metrics for each rep
140
+ feedback = "" # Real-time feedback for the video feed
141
+ workout_start_time = None # Timer start
142
+
143
+ with mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5) as pose:
144
+ while cap.isOpened():
145
+ ret, frame = cap.read()
146
+ if not ret:
147
+ print("Failed to grab frame.")
148
+ break
149
+
150
+ # Initialize workout start time
151
+ if workout_start_time is None:
152
+ workout_start_time = time.time()
153
+
154
+ # Timer
155
+ elapsed_time = time.time() - workout_start_time
156
+ timer_text = f"Timer: {int(elapsed_time)}s"
157
+
158
+ # Convert frame to RGB
159
+ image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
160
+ image.flags.writeable = False
161
+ results = pose.process(image)
162
+
163
+ # Convert back to BGR
164
+ image.flags.writeable = True
165
+ image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
166
+
167
+ # Check if pose landmarks are detected
168
+ if results.pose_landmarks:
169
+ landmarks = results.pose_landmarks.landmark
170
+
171
+ # Extract key joints
172
+ shoulder = [
173
+ landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x,
174
+ landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y
175
+ ]
176
+ elbow = [
177
+ landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x,
178
+ landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].y
179
+ ]
180
+ wrist = [
181
+ landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x,
182
+ landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y
183
+ ]
184
+
185
+ # Check visibility of key joints
186
+ visibility_threshold = 0.5
187
+ if (landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].visibility < visibility_threshold or
188
+ landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].visibility < visibility_threshold or
189
+ landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].visibility < visibility_threshold):
190
+ draw_text_with_background(image, "Ensure all key joints are visible!", (50, 150),
191
+ cv2.FONT_HERSHEY_SIMPLEX, 2, (255, 255, 255), 5, (0, 0, 255))
192
+ cv2.imshow('Workout Feedback', image)
193
+ continue # Skip processing if joints are not visible
194
+
195
+ # Calculate the angle
196
+ angle = calculate_angle(shoulder, elbow, wrist)
197
+
198
+ # Stage logic for counting reps
199
+ if angle > 160 and stage != "down":
200
+ stage = "down"
201
+ start_time = time.time() # Start timing for the rep
202
+ start_angle = angle # Record the starting angle
203
+
204
+ # Stop the program if it's the 10th rep down stage
205
+ if counter == max_reps:
206
+ print("Workout complete at rep 10 (down stage)!")
207
+ break
208
+ elif angle < 40 and stage == "down":
209
+ stage = "up"
210
+ counter += 1
211
+ end_time = time.time() # End timing for the rep
212
+ end_angle = angle # Record the ending angle
213
+
214
+ # Calculate rep metrics
215
+ rom = start_angle - end_angle # Range of Motion
216
+ tempo = end_time - start_time # Duration of the rep
217
+ smoothness = np.std([start_angle, end_angle]) # Dummy smoothness metric
218
+ rep_data.append({"ROM": rom, "Tempo": tempo, "Smoothness": smoothness})
219
+
220
+ # Analyze the rep using Isolation Forest
221
+ feedback = analyze_single_rep(rep_data[-1], rep_data)
222
+
223
+ # Wireframe color based on form
224
+ wireframe_color = (0, 255, 0) if stage == "up" or stage == "down" else (0, 0, 255)
225
+
226
+ # Draw wireframe
227
+ mp_drawing.draw_landmarks(
228
+ image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS,
229
+ mp_drawing.DrawingSpec(color=wireframe_color, thickness=5, circle_radius=4),
230
+ mp_drawing.DrawingSpec(color=wireframe_color, thickness=5, circle_radius=4)
231
+ )
232
+
233
+ # Display reps, stage, timer, and feedback
234
+ draw_text_with_background(image, f"Reps: {counter}", (50, 150),
235
+ cv2.FONT_HERSHEY_SIMPLEX, 3, (255, 255, 255), 5, (0, 0, 0))
236
+ draw_text_with_background(image, f"Stage: {stage if stage else 'N/A'}", (50, 300),
237
+ cv2.FONT_HERSHEY_SIMPLEX, 3, (255, 255, 255), 5, (0, 0, 0))
238
+ draw_text_with_background(image, timer_text, (1000, 50), # Timer in the top-right corner
239
+ cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 3, (0, 0, 0))
240
+ draw_text_with_background(image, feedback, (50, 450),
241
+ cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 3, (0, 0, 0))
242
+
243
+ # Show video feed
244
+ cv2.imshow('Workout Feedback', image)
245
+
246
+ # Break if 'q' is pressed
247
+ if cv2.waitKey(10) & 0xFF == ord('q'):
248
+ break
249
+
250
+ cap.release()
251
+ cv2.destroyAllWindows()
252
+
253
+ # Post-workout analysis
254
+ analyze_workout_with_isolation_forest(rep_data)
255
+
256
+
257
+ if __name__ == "__main__":
258
+ main()
259
+
260
+
261
+ # From lateral_raise.py
262
+ import cv2
263
+ import mediapipe as mp
264
+ import numpy as np
265
+ import time
266
+ from sklearn.ensemble import IsolationForest
267
+
268
+ # Mediapipe utilities
269
+ mp_drawing = mp.solutions.drawing_utils
270
+ mp_pose = mp.solutions.pose
271
+
272
+
273
+ # Function to calculate lateral raise angle
274
+ def calculate_angle_for_lateral_raise(shoulder, wrist):
275
+ """
276
+ Calculate the angle of the arm relative to the horizontal plane
277
+ passing through the shoulder.
278
+ """
279
+ horizontal_reference = np.array([1, 0]) # Horizontal vector
280
+ arm_vector = np.array([wrist[0] - shoulder[0], wrist[1] - shoulder[1]])
281
+ dot_product = np.dot(horizontal_reference, arm_vector)
282
+ magnitude_reference = np.linalg.norm(horizontal_reference)
283
+ magnitude_arm = np.linalg.norm(arm_vector)
284
+ if magnitude_arm == 0 or magnitude_reference == 0:
285
+ return 0
286
+ cos_angle = dot_product / (magnitude_reference * magnitude_arm)
287
+ angle = np.arccos(np.clip(cos_angle, -1.0, 1.0))
288
+ return np.degrees(angle)
289
+
290
+
291
+ # Function to draw text with a background
292
+ def draw_text_with_background(image, text, position, font, font_scale, color, thickness, bg_color, padding=10):
293
+ text_size = cv2.getTextSize(text, font, font_scale, thickness)[0]
294
+ text_x, text_y = position
295
+ box_coords = (
296
+ (text_x - padding, text_y - padding),
297
+ (text_x + text_size[0] + padding, text_y + text_size[1] + padding),
298
+ )
299
+ cv2.rectangle(image, box_coords[0], box_coords[1], bg_color, cv2.FILLED)
300
+ cv2.putText(image, text, (text_x, text_y + text_size[1]), font, font_scale, color, thickness)
301
+
302
+
303
+ # Function to check if all required joints are visible
304
+ def are_key_joints_visible(landmarks, visibility_threshold=0.5):
305
+ """
306
+ Ensure that all required joints are visible based on their visibility scores.
307
+ """
308
+ required_joints = [
309
+ mp_pose.PoseLandmark.LEFT_SHOULDER.value,
310
+ mp_pose.PoseLandmark.RIGHT_SHOULDER.value,
311
+ mp_pose.PoseLandmark.LEFT_WRIST.value,
312
+ mp_pose.PoseLandmark.RIGHT_WRIST.value,
313
+ ]
314
+ for joint in required_joints:
315
+ if landmarks[joint].visibility < visibility_threshold:
316
+ return False
317
+ return True
318
+
319
+
320
+ # Real-time feedback for single rep
321
+ def analyze_single_rep(rep, rep_data):
322
+ """Provide actionable feedback for a single rep."""
323
+ feedback = []
324
+
325
+ # Calculate averages from previous reps
326
+ avg_rom = np.mean([r["ROM"] for r in rep_data]) if rep_data else 0
327
+ avg_tempo = np.mean([r["Tempo"] for r in rep_data]) if rep_data else 0
328
+
329
+ # Dynamic tempo thresholds
330
+ lower_tempo_threshold = 2.0 # Minimum grace threshold for faster tempo
331
+ upper_tempo_threshold = 9.0 # Maximum grace threshold for slower tempo
332
+
333
+ # Adjust thresholds after a few reps
334
+ if len(rep_data) > 3:
335
+ lower_tempo_threshold = max(2.0, avg_tempo * 0.7)
336
+ upper_tempo_threshold = min(9.0, avg_tempo * 1.3)
337
+
338
+ # Feedback for ROM
339
+ if rep["ROM"] < 30: # Minimum ROM threshold
340
+ feedback.append("Lift arm higher")
341
+ elif rep_data and rep["ROM"] < avg_rom * 0.8:
342
+ feedback.append("Increase ROM")
343
+
344
+ # Feedback for Tempo
345
+ if rep["Tempo"] < lower_tempo_threshold: # Tempo too fast
346
+ feedback.append("Slow down")
347
+ elif rep["Tempo"] > upper_tempo_threshold: # Tempo too slow
348
+ feedback.append("Speed up")
349
+
350
+ return feedback
351
+
352
+
353
+ # Post-workout feedback function
354
+ def analyze_workout_with_isolation_forest(rep_data):
355
+ if not rep_data:
356
+ print("No reps completed.")
357
+ return
358
+
359
+ print("\n--- Post-Workout Summary ---")
360
+
361
+ # Filter valid reps for recalculating thresholds
362
+ valid_reps = [rep for rep in rep_data if rep["ROM"] > 20] # Ignore very low ROM reps
363
+
364
+ if not valid_reps:
365
+ print("No valid reps to analyze.")
366
+ return
367
+
368
+ features = np.array([[rep["ROM"], rep["Tempo"]] for rep in valid_reps])
369
+
370
+ avg_rom = np.mean(features[:, 0])
371
+ avg_tempo = np.mean(features[:, 1])
372
+ std_rom = np.std(features[:, 0])
373
+ std_tempo = np.std(features[:, 1])
374
+
375
+ # Adjusted bounds for anomalies
376
+ rom_lower_bound = max(20, avg_rom - std_rom * 2)
377
+ tempo_lower_bound = max(1.0, avg_tempo - std_tempo * 2)
378
+ tempo_upper_bound = min(10.0, avg_tempo + std_tempo * 2)
379
+
380
+ print(f"ROM Lower Bound: {rom_lower_bound}")
381
+ print(f"Tempo Bounds: {tempo_lower_bound}-{tempo_upper_bound}")
382
+
383
+ # Anomaly detection
384
+ for i, rep in enumerate(valid_reps, 1):
385
+ feedback = []
386
+ if rep["ROM"] < rom_lower_bound:
387
+ feedback.append("Low ROM")
388
+ if rep["Tempo"] < tempo_lower_bound:
389
+ feedback.append("Too Fast")
390
+ elif rep["Tempo"] > tempo_upper_bound:
391
+ feedback.append("Too Slow")
392
+
393
+ if feedback:
394
+ print(f"Rep {i}: Anomalous | Feedback: {', '.join(feedback[:1])}")
395
+
396
+ # Use Isolation Forest for secondary anomaly detection
397
+ model = IsolationForest(contamination=0.1, random_state=42) # Reduced contamination
398
+ predictions = model.fit_predict(features)
399
+
400
+ for i, prediction in enumerate(predictions, 1):
401
+ if prediction == -1: # Outlier
402
+ print(f"Rep {i}: Isolation Forest flagged this rep as anomalous.")
403
+
404
+
405
+ # Main workout tracking function
406
+ def main():
407
+ cap = cv2.VideoCapture(0)
408
+ counter = 0 # Rep counter
409
+ stage = None # Movement stage
410
+ feedback = [] # Real-time feedback for the video feed
411
+ rep_data = [] # Store metrics for each rep
412
+ angles_during_rep = [] # Track angles during a single rep
413
+ workout_start_time = None # Timer start
414
+
415
+ with mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5) as pose:
416
+ while cap.isOpened():
417
+ ret, frame = cap.read()
418
+ if not ret:
419
+ print("Failed to grab frame.")
420
+ break
421
+
422
+ # Initialize workout start time
423
+ if workout_start_time is None:
424
+ workout_start_time = time.time()
425
+
426
+ # Timer
427
+ elapsed_time = time.time() - workout_start_time
428
+ timer_text = f"Timer: {int(elapsed_time)}s"
429
+
430
+ # Convert the image to RGB
431
+ image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
432
+ image.flags.writeable = False
433
+ results = pose.process(image)
434
+
435
+ # Convert back to BGR
436
+ image.flags.writeable = True
437
+ image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
438
+
439
+ # Check if pose landmarks are detected
440
+ if results.pose_landmarks:
441
+ landmarks = results.pose_landmarks.landmark
442
+
443
+ # Check if key joints are visible
444
+ if not are_key_joints_visible(landmarks):
445
+ draw_text_with_background(
446
+ image, "Ensure all joints are visible", (50, 50),
447
+ cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, (0, 0, 255)
448
+ )
449
+ cv2.imshow("Lateral Raise Tracker", image)
450
+ continue
451
+
452
+ # Extract key joints
453
+ left_shoulder = [
454
+ landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x,
455
+ landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y,
456
+ ]
457
+ left_wrist = [
458
+ landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x,
459
+ landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y,
460
+ ]
461
+
462
+ # Calculate angle for lateral raise
463
+ angle = calculate_angle_for_lateral_raise(left_shoulder, left_wrist)
464
+
465
+ # Track angles during a rep
466
+ if stage == "up" or stage == "down":
467
+ angles_during_rep.append(angle)
468
+
469
+ # Stage logic for counting reps
470
+ if angle < 20 and stage != "down":
471
+ stage = "down"
472
+ if counter == 10: # Stop on the down stage of the 10th rep
473
+ print("Workout complete! 10 reps reached.")
474
+ break
475
+
476
+ # Calculate ROM for the completed rep
477
+ if len(angles_during_rep) > 1:
478
+ rom = max(angles_during_rep) - min(angles_during_rep)
479
+ else:
480
+ rom = 0.0
481
+
482
+ tempo = elapsed_time
483
+ print(f"Rep {counter + 1}: ROM={rom:.2f}, Tempo={tempo:.2f}s")
484
+
485
+ # Record metrics for the rep
486
+ rep_data.append({
487
+ "ROM": rom,
488
+ "Tempo": tempo,
489
+ })
490
+
491
+ # Reset angles and timer for the next rep
492
+ angles_during_rep = []
493
+ workout_start_time = time.time() # Reset timer
494
+
495
+ if 70 <= angle <= 110 and stage == "down":
496
+ stage = "up"
497
+ counter += 1
498
+
499
+ # Analyze feedback
500
+ feedback = analyze_single_rep(rep_data[-1], rep_data)
501
+
502
+ # Determine wireframe color
503
+ wireframe_color = (0, 255, 0) if not feedback else (0, 0, 255)
504
+
505
+ # Display feedback
506
+ draw_text_with_background(image, f"Reps: {counter}", (50, 50),
507
+ cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, (0, 0, 0))
508
+ draw_text_with_background(image, " | ".join(feedback), (50, 120),
509
+ cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, (0, 0, 0))
510
+ draw_text_with_background(image, timer_text, (50, 190),
511
+ cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, (0, 0, 0))
512
+
513
+ # Render detections with wireframe color
514
+ mp_drawing.draw_landmarks(
515
+ image,
516
+ results.pose_landmarks,
517
+ mp_pose.POSE_CONNECTIONS,
518
+ mp_drawing.DrawingSpec(color=wireframe_color, thickness=2, circle_radius=2),
519
+ mp_drawing.DrawingSpec(color=wireframe_color, thickness=2, circle_radius=2),
520
+ )
521
+
522
+ # Display the image
523
+ cv2.imshow("Lateral Raise Tracker", image)
524
+
525
+ if cv2.waitKey(10) & 0xFF == ord("q"):
526
+ break
527
+
528
+ cap.release()
529
+ cv2.destroyAllWindows()
530
+
531
+ # Post-workout analysis
532
+ analyze_workout_with_isolation_forest(rep_data)
533
+
534
+
535
+ if __name__ == "__main__":
536
+ main()
537
+
538
+
539
+ # From shoulder_press.py
540
+ import cv2
541
+ import mediapipe as mp
542
+ import numpy as np
543
+ import time
544
+
545
+ # Mediapipe utilities
546
+ mp_drawing = mp.solutions.drawing_utils
547
+ mp_pose = mp.solutions.pose
548
+
549
+ # Function to calculate angles
550
+ def calculate_angle(point_a, point_b, point_c):
551
+ vector_ab = np.array([point_a[0] - point_b[0], point_a[1] - point_b[1]])
552
+ vector_cb = np.array([point_c[0] - point_b[0], point_c[1] - point_b[1]])
553
+ dot_product = np.dot(vector_ab, vector_cb)
554
+ magnitude_ab = np.linalg.norm(vector_ab)
555
+ magnitude_cb = np.linalg.norm(vector_cb)
556
+ if magnitude_ab == 0 or magnitude_cb == 0:
557
+ return 0
558
+ cos_angle = dot_product / (magnitude_ab * magnitude_cb)
559
+ angle = np.arccos(np.clip(cos_angle, -1.0, 1.0))
560
+ return np.degrees(angle)
561
+
562
+
563
+ # Function to check if all required joints are visible
564
+ def are_key_joints_visible(landmarks, visibility_threshold=0.5):
565
+ required_joints = [
566
+ mp_pose.PoseLandmark.LEFT_SHOULDER.value,
567
+ mp_pose.PoseLandmark.RIGHT_SHOULDER.value,
568
+ mp_pose.PoseLandmark.LEFT_ELBOW.value,
569
+ mp_pose.PoseLandmark.RIGHT_ELBOW.value,
570
+ mp_pose.PoseLandmark.LEFT_WRIST.value,
571
+ mp_pose.PoseLandmark.RIGHT_WRIST.value,
572
+ ]
573
+ for joint in required_joints:
574
+ if landmarks[joint].visibility < visibility_threshold:
575
+ return False
576
+ return True
577
+
578
+
579
+ # Function to draw text with a background
580
+ def draw_text_with_background(image, text, position, font, font_scale, color, thickness, bg_color, padding=10):
581
+ text_size = cv2.getTextSize(text, font, font_scale, thickness)[0]
582
+ text_x, text_y = position
583
+ box_coords = (
584
+ (text_x - padding, text_y - padding),
585
+ (text_x + text_size[0] + padding, text_y + text_size[1] + padding),
586
+ )
587
+ cv2.rectangle(image, box_coords[0], box_coords[1], bg_color, cv2.FILLED)
588
+ cv2.putText(image, text, (text_x, text_y + text_size[1]), font, font_scale, color, thickness)
589
+
590
+
591
+ # Main workout tracking function
592
+ def main():
593
+ cap = cv2.VideoCapture(0)
594
+ counter = 0
595
+ stage = None
596
+ feedback = ""
597
+ workout_start_time = None
598
+ rep_start_time = None
599
+
600
+ with mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5) as pose:
601
+ while cap.isOpened():
602
+ ret, frame = cap.read()
603
+ if not ret:
604
+ print("Failed to grab frame.")
605
+ break
606
+
607
+ # Initialize workout start time
608
+ if workout_start_time is None:
609
+ workout_start_time = time.time()
610
+
611
+ # Timer
612
+ elapsed_time = time.time() - workout_start_time
613
+ timer_text = f"Timer: {int(elapsed_time)}s"
614
+
615
+ # Convert the image to RGB
616
+ image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
617
+ image.flags.writeable = False
618
+ results = pose.process(image)
619
+
620
+ # Convert back to BGR
621
+ image.flags.writeable = True
622
+ image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
623
+
624
+ # Check if pose landmarks are detected
625
+ if results.pose_landmarks:
626
+ landmarks = results.pose_landmarks.landmark
627
+
628
+ # Check if key joints are visible
629
+ if not are_key_joints_visible(landmarks):
630
+ feedback = "Ensure all joints are visible"
631
+ draw_text_with_background(
632
+ image, feedback, (50, 50),
633
+ cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, (0, 0, 255)
634
+ )
635
+ cv2.imshow("Shoulder Press Tracker", image)
636
+ continue
637
+
638
+ # Extract key joints for both arms
639
+ left_shoulder = [
640
+ landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x,
641
+ landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y,
642
+ ]
643
+ left_elbow = [
644
+ landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x,
645
+ landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].y,
646
+ ]
647
+ left_wrist = [
648
+ landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x,
649
+ landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y,
650
+ ]
651
+
652
+ right_shoulder = [
653
+ landmarks[mp_pose.PoseLandmark.RIGHT_SHOULDER.value].x,
654
+ landmarks[mp_pose.PoseLandmark.RIGHT_SHOULDER.value].y,
655
+ ]
656
+ right_elbow = [
657
+ landmarks[mp_pose.PoseLandmark.RIGHT_ELBOW.value].x,
658
+ landmarks[mp_pose.PoseLandmark.RIGHT_ELBOW.value].y,
659
+ ]
660
+ right_wrist = [
661
+ landmarks[mp_pose.PoseLandmark.RIGHT_WRIST.value].x,
662
+ landmarks[mp_pose.PoseLandmark.RIGHT_WRIST.value].y,
663
+ ]
664
+
665
+ # Calculate angles
666
+ left_elbow_angle = calculate_angle(left_shoulder, left_elbow, left_wrist)
667
+ right_elbow_angle = calculate_angle(right_shoulder, right_elbow, right_wrist)
668
+
669
+ # Check starting and ending positions
670
+ if 80 <= left_elbow_angle <= 100 and 80 <= right_elbow_angle <= 100 and stage != "down":
671
+ stage = "down"
672
+ if counter == 10:
673
+ feedback = "Workout complete! 10 reps done."
674
+ draw_text_with_background(image, feedback, (50, 120),
675
+ cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, (0, 0, 255))
676
+ cv2.imshow("Shoulder Press Tracker", image)
677
+ break
678
+ if rep_start_time is not None:
679
+ tempo = time.time() - rep_start_time
680
+ feedback = f"Rep {counter} completed! Tempo: {tempo:.2f}s"
681
+ rep_start_time = None
682
+ elif left_elbow_angle > 160 and right_elbow_angle > 160 and stage == "down":
683
+ stage = "up"
684
+ counter += 1
685
+ rep_start_time = time.time()
686
+
687
+ # Wireframe color
688
+ wireframe_color = (0, 255, 0) if "completed" in feedback or "Good" in feedback else (0, 0, 255)
689
+
690
+ # Display feedback
691
+ draw_text_with_background(image, f"Reps: {counter}", (50, 50),
692
+ cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, (0, 0, 0))
693
+ draw_text_with_background(image, feedback, (50, 120),
694
+ cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, (0, 0, 0))
695
+ draw_text_with_background(image, timer_text, (50, 190),
696
+ cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, (0, 0, 0))
697
+
698
+ # Render detections with wireframe color
699
+ mp_drawing.draw_landmarks(
700
+ image,
701
+ results.pose_landmarks,
702
+ mp_pose.POSE_CONNECTIONS,
703
+ mp_drawing.DrawingSpec(color=wireframe_color, thickness=2, circle_radius=2),
704
+ mp_drawing.DrawingSpec(color=wireframe_color, thickness=2, circle_radius=2),
705
+ )
706
+
707
+ # Display the image
708
+ cv2.imshow("Shoulder Press Tracker", image)
709
+
710
+ if cv2.waitKey(10) & 0xFF == ord("q"):
711
+ break
712
+
713
+ cap.release()
714
+ cv2.destroyAllWindows()
715
+
716
+
717
+ if __name__ == "__main__":
718
+ main()
719
+
720
+