zforkash commited on
Commit
6b52889
·
verified ·
1 Parent(s): 8afa4d1

back to update 1

Browse files
Files changed (1) hide show
  1. app.py +701 -311
app.py CHANGED
@@ -1,331 +1,721 @@
 
1
  import streamlit as st
2
- from streamlit_webrtc import webrtc_streamer, WebRtcMode, RTCConfiguration
3
- import av
4
- import numpy as np
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  import mediapipe as mp
6
- from typing import List, Tuple, Dict
7
- import logging
8
- from PIL import Image
9
- import queue
10
- import threading
11
- import logging
12
- import sys
13
-
14
- try:
15
- from typing import Literal
16
- except ImportError:
17
- from typing_extensions import Literal
18
-
19
- # Configure logging
20
- logging.basicConfig(level=logging.DEBUG)
21
- logger = logging.getLogger(__name__)
22
-
23
- # Initialize MediaPipe Pose
24
  mp_pose = mp.solutions.pose
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  mp_drawing = mp.solutions.drawing_utils
 
26
 
27
- # Page config
28
- st.set_page_config(page_title="AI Workout Trainer", page_icon="💪")
29
 
30
- # Constants
31
- RTC_CONFIGURATION = RTCConfiguration(
32
- {"iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]}
33
- )
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
- # CSS
36
- st.markdown("""
37
- <style>
38
- .main {
39
- background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
40
- }
41
- .stButton > button {
42
- width: 100%;
43
- background-color: #2a5298;
44
- color: white;
45
- border-radius: 5px;
46
- padding: 10px;
47
- margin: 5px 0;
48
- }
49
- .workout-container {
50
- background-color: rgba(255, 255, 255, 0.1);
51
- padding: 20px;
52
- border-radius: 10px;
53
- margin: 10px 0;
54
- }
55
- h1, h2, h3 {
56
- color: white;
57
- }
58
- </style>
59
- """, unsafe_allow_html=True)
60
-
61
- class PoseTracker:
62
- def __init__(self):
63
- self.pose = mp_pose.Pose(
64
- min_detection_confidence=0.5,
65
- min_tracking_confidence=0.5
66
- )
67
- self.counter = 0
68
- self.stage = None
69
- self.feedback = ""
70
-
71
- def calculate_angle(self, a: np.ndarray, b: np.ndarray, c: np.ndarray) -> float:
72
- a = np.array(a)
73
- b = np.array(b)
74
- c = np.array(c)
75
-
76
- radians = np.arctan2(c[1]-b[1], c[0]-b[0]) - \
77
- np.arctan2(a[1]-b[1], a[0]-b[0])
78
- angle = np.abs(np.degrees(radians))
79
-
80
- if angle > 180.0:
81
- angle = 360-angle
82
-
83
- return angle
84
-
85
- def process_frame(self, frame: np.ndarray, exercise_type: str) -> Tuple[np.ndarray, str]:
86
- try:
87
- # Convert BGR to RGB
88
- image = frame.copy()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  image.flags.writeable = False
90
- image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
91
- results = self.pose.process(image)
92
-
93
- # Draw landmarks
94
  image.flags.writeable = True
95
  image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
96
-
 
97
  if results.pose_landmarks:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  mp_drawing.draw_landmarks(
99
  image,
100
  results.pose_landmarks,
101
- mp_pose.POSE_CONNECTIONS
 
 
102
  )
103
-
104
- # Extract landmarks
105
- try:
106
- landmarks = results.pose_landmarks.landmark
107
-
108
- # Process based on exercise type
109
- if exercise_type == "bicep_curl":
110
- feedback = self._process_bicep_curl(landmarks, image)
111
- elif exercise_type == "shoulder_press":
112
- feedback = self._process_shoulder_press(landmarks, image)
113
- elif exercise_type == "lateral_raise":
114
- feedback = self._process_lateral_raise(landmarks, image)
115
- else:
116
- feedback = "Unknown exercise type"
117
-
118
- return image, feedback
119
-
120
- except Exception as e:
121
- logger.error(f"Error processing landmarks: {str(e)}")
122
- return image, "Error processing pose"
123
-
124
- return image, "No pose detected"
125
-
126
- except Exception as e:
127
- logger.error(f"Error in process_frame: {str(e)}")
128
- return frame, "Error processing frame"
129
-
130
- def _process_bicep_curl(self, landmarks, image) -> str:
131
- # Get coordinates
132
- shoulder = [landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x,
133
- landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y]
134
- elbow = [landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x,
135
- landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].y]
136
- wrist = [landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x,
137
- landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y]
138
-
139
- # Calculate angle
140
- angle = self.calculate_angle(shoulder, elbow, wrist)
141
-
142
- # Counter logic
143
- if angle > 160:
144
- self.stage = "down"
145
- elif angle < 30 and self.stage == "down":
146
- self.stage = "up"
147
- self.counter += 1
148
-
149
- # Add text to image
150
- cv2.putText(image, f'Angle: {angle:.2f}', (10,30),
151
- cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 2)
152
- cv2.putText(image, f'Count: {self.counter}', (10,60),
153
- cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 2)
154
-
155
- if angle < 160 and self.stage == "down":
156
- return "Curl up"
157
- elif angle > 30 and self.stage == "up":
158
- return "Lower the weight"
159
- else:
160
- return f"Count: {self.counter}"
161
-
162
- def _process_shoulder_press(self, landmarks, image) -> str:
163
- # Similar structure to bicep curl but with shoulder press specific angles
164
- shoulder = [landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x,
165
- landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y]
166
- elbow = [landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x,
167
- landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].y]
168
- wrist = [landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x,
169
- landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y]
170
-
171
- angle = self.calculate_angle(shoulder, elbow, wrist)
172
-
173
- if angle < 90:
174
- self.stage = "down"
175
- elif angle > 160 and self.stage == "down":
176
- self.stage = "up"
177
- self.counter += 1
178
-
179
- cv2.putText(image, f'Angle: {angle:.2f}', (10,30),
180
- cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 2)
181
- cv2.putText(image, f'Count: {self.counter}', (10,60),
182
- cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 2)
183
-
184
- if angle > 90 and self.stage == "down":
185
- return "Press up fully"
186
- elif angle < 160 and self.stage == "up":
187
- return "Lower the weight"
188
- else:
189
- return f"Count: {self.counter}"
190
-
191
- def _process_lateral_raise(self, landmarks, image) -> str:
192
- shoulder = [landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x,
193
- landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y]
194
- elbow = [landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x,
195
- landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].y]
196
- wrist = [landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x,
197
- landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y]
198
-
199
- angle = self.calculate_angle(shoulder, elbow, wrist)
200
-
201
- if angle < 20:
202
- self.stage = "down"
203
- elif angle > 80 and self.stage == "down":
204
- self.stage = "up"
205
- self.counter += 1
206
-
207
- cv2.putText(image, f'Angle: {angle:.2f}', (10,30),
208
- cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 2)
209
- cv2.putText(image, f'Count: {self.counter}', (10,60),
210
- cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 2)
211
-
212
- if angle < 80 and self.stage == "down":
213
- return "Raise arms higher"
214
- elif angle > 20 and self.stage == "up":
215
- return "Lower arms"
216
- else:
217
- return f"Count: {self.counter}"
218
-
219
- class VideoProcessor:
220
- def __init__(self) -> None:
221
- self.pose_tracker = PoseTracker()
222
- self._exercise_type = "bicep_curl"
223
-
224
- @property
225
- def exercise_type(self) -> str:
226
- return self._exercise_type
227
-
228
- @exercise_type.setter
229
- def exercise_type(self, value: str) -> None:
230
- self._exercise_type = value
231
-
232
- def recv(self, frame: av.VideoFrame) -> av.VideoFrame:
233
- try:
234
- img = frame.to_ndarray(format="bgr24")
235
-
236
- # Process the frame
237
- processed_frame, feedback = self.pose_tracker.process_frame(img, self.exercise_type)
238
-
239
- # Add feedback to frame
240
- cv2.putText(processed_frame, feedback, (10,90),
241
- cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 2)
242
-
243
- return av.VideoFrame.from_ndarray(processed_frame, format="bgr24")
244
- except Exception as e:
245
- logger.error(f"Error in recv: {str(e)}")
246
- return frame
247
 
248
- def main():
249
- # Title and description
250
- st.title("🏋️‍♂️ AI Workout Trainer")
251
- st.markdown("""
252
- <div class="workout-container">
253
- <p>Welcome to your personal AI workout trainer! This app will help you:
254
- <ul>
255
- <li>Track your exercise form in real-time</li>
256
- <li>Count your reps automatically</li>
257
- <li>Provide instant feedback on your technique</li>
258
- </ul></p>
259
- </div>
260
- """, unsafe_allow_html=True)
261
-
262
- # Exercise selection
263
- exercise_type = st.selectbox(
264
- "Choose your exercise:",
265
- ["bicep_curl", "shoulder_press", "lateral_raise"],
266
- format_func=lambda x: x.replace('_', ' ').title()
267
- )
268
-
269
- # Create VideoProcessor instance
270
- ctx = webrtc_streamer(
271
- key="workout-tracker",
272
- mode=WebRtcMode.SENDRECV,
273
- rtc_configuration=RTC_CONFIGURATION,
274
- video_processor_factory=VideoProcessor,
275
- media_stream_constraints={"video": True, "audio": False},
276
- async_processing=True,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  )
278
-
279
- # Update exercise type when changed
280
- if ctx.video_processor:
281
- ctx.video_processor.exercise_type = exercise_type
282
-
283
- # Instructions based on exercise
284
- if exercise_type == "bicep_curl":
285
- st.markdown("""
286
- <div class="workout-container">
287
- <h3>Bicep Curl Instructions:</h3>
288
- <ul>
289
- <li>Stand straight with weights at your sides</li>
290
- <li>Keep your upper arms still</li>
291
- <li>Curl the weights up towards your shoulders</li>
292
- <li>Lower the weights back down controlled</li>
293
- </ul>
294
- </div>
295
- """, unsafe_allow_html=True)
296
- elif exercise_type == "shoulder_press":
297
- st.markdown("""
298
- <div class="workout-container">
299
- <h3>Shoulder Press Instructions:</h3>
300
- <ul>
301
- <li>Start with weights at shoulder height</li>
302
- <li>Press weights straight up overhead</li>
303
- <li>Keep your core tight</li>
304
- <li>Lower weights back to shoulders controlled</li>
305
- </ul>
306
- </div>
307
- """, unsafe_allow_html=True)
308
- else: # lateral_raise
309
- st.markdown("""
310
- <div class="workout-container">
311
- <h3>Lateral Raise Instructions:</h3>
312
- <ul>
313
- <li>Stand straight with weights at your sides</li>
314
- <li>Raise arms out to sides up to shoulder height</li>
315
- <li>Keep a slight bend in your elbows</li>
316
- <li>Lower weights back down controlled</li>
317
- </ul>
318
- </div>
319
- """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
 
321
  if __name__ == "__main__":
322
- try:
323
- # Import OpenCV
324
- import cv2
325
- main()
326
- except Exception as e:
327
- st.error(f"An error occurred: {str(e)}")
328
- if "cv2" in str(e):
329
- st.warning("OpenCV import failed. Please check your installation.")
330
- logger.error(f"Application error: {str(e)}")
331
 
 
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
+
 
 
 
 
 
 
721