import streamlit as st import cv2 import mediapipe as mp import numpy as np import time from sklearn.ensemble import IsolationForest # Encapsulated workout functions def bicep_curl(): import cv2 import mediapipe as mp import numpy as np import time from sklearn.ensemble import IsolationForest # Mediapipe utilities mp_drawing = mp.solutions.drawing_utils mp_pose = mp.solutions.pose # Function to calculate angles between three points def calculate_angle(a, b, c): a = np.array(a) b = np.array(b) c = np.array(c) radians = np.arctan2(c[1] - b[1], c[0] - b[0]) - np.arctan2(a[1] - b[1], a[0] - b[0]) angle = np.abs(np.degrees(radians)) if angle > 180.0: angle = 360 - angle return angle # Function to draw text with a background def draw_text_with_background(image, text, position, font, font_scale, color, thickness, bg_color, padding=10): text_size = cv2.getTextSize(text, font, font_scale, thickness)[0] text_x, text_y = position box_coords = ( (text_x - padding, text_y - padding), (text_x + text_size[0] + padding, text_y + text_size[1] + padding), ) cv2.rectangle(image, box_coords[0], box_coords[1], bg_color, cv2.FILLED) cv2.putText(image, text, (text_x, text_y + text_size[1]), font, font_scale, color, thickness) # Real-time feedback for single rep def analyze_single_rep(rep, rep_data): """Provide actionable feedback for a single rep.""" feedback = [] avg_rom = np.mean([r["ROM"] for r in rep_data]) avg_tempo = np.mean([r["Tempo"] for r in rep_data]) avg_smoothness = np.mean([r["Smoothness"] for r in rep_data]) if rep["ROM"] < avg_rom * 0.8: feedback.append("Extend arm more") if rep["Tempo"] < avg_tempo * 0.8: feedback.append("Slow down") if rep["Smoothness"] > avg_smoothness * 1.2: feedback.append("Move smoothly") return " | ".join(feedback) if feedback else "Good rep!" # Post-workout feedback function with Isolation Forest def analyze_workout_with_isolation_forest(rep_data): if not rep_data: print("No reps completed.") return print("\n--- Post-Workout Summary ---") # Convert rep_data to a feature matrix features = np.array([[rep["ROM"], rep["Tempo"], rep["Smoothness"]] for rep in rep_data]) # Train Isolation Forest model = IsolationForest(contamination=0.2, random_state=42) predictions = model.fit_predict(features) # Analyze reps for i, (rep, prediction) in enumerate(zip(rep_data, predictions), 1): status = "Good" if prediction == 1 else "Anomalous" reason = [] if prediction == -1: # If anomalous if rep["ROM"] < np.mean(features[:, 0]) - np.std(features[:, 0]): reason.append("Low ROM") if rep["Tempo"] < np.mean(features[:, 1]) - np.std(features[:, 1]): reason.append("Too Fast") if rep["Smoothness"] > np.mean(features[:, 2]) + np.std(features[:, 2]): reason.append("Jerky Movement") reason_str = ", ".join(reason) if reason else "None" print(f"Rep {i}: {status} | ROM: {rep['ROM']:.2f}, Tempo: {rep['Tempo']:.2f}s, Smoothness: {rep['Smoothness']:.2f} | Reason: {reason_str}") # Main workout tracking function def main(): cap = cv2.VideoCapture(0) counter = 0 # Rep counter stage = None # Movement stage max_reps = 10 rep_data = [] # Store metrics for each rep feedback = "" # Real-time feedback for the video feed workout_start_time = None # Timer start with mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5) as pose: while cap.isOpened(): ret, frame = cap.read() if not ret: print("Failed to grab frame.") break # Initialize workout start time if workout_start_time is None: workout_start_time = time.time() # Timer elapsed_time = time.time() - workout_start_time timer_text = f"Timer: {int(elapsed_time)}s" # Convert frame to RGB image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) image.flags.writeable = False results = pose.process(image) # Convert back to BGR image.flags.writeable = True image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) # Check if pose landmarks are detected if results.pose_landmarks: landmarks = results.pose_landmarks.landmark # Extract key joints shoulder = [ landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x, landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y ] elbow = [ landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x, landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].y ] wrist = [ landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x, landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y ] # Check visibility of key joints visibility_threshold = 0.5 if (landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].visibility < visibility_threshold or landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].visibility < visibility_threshold or landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].visibility < visibility_threshold): draw_text_with_background(image, "Ensure all key joints are visible!", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 2, (255, 255, 255), 5, (0, 0, 255)) cv2.imshow('Workout Feedback', image) continue # Skip processing if joints are not visible # Calculate the angle angle = calculate_angle(shoulder, elbow, wrist) # Stage logic for counting reps if angle > 160 and stage != "down": stage = "down" start_time = time.time() # Start timing for the rep start_angle = angle # Record the starting angle # Stop the program if it's the 10th rep down stage if counter == max_reps: print("Workout complete at rep 10 (down stage)!") break elif angle < 40 and stage == "down": stage = "up" counter += 1 end_time = time.time() # End timing for the rep end_angle = angle # Record the ending angle # Calculate rep metrics rom = start_angle - end_angle # Range of Motion tempo = end_time - start_time # Duration of the rep smoothness = np.std([start_angle, end_angle]) # Dummy smoothness metric rep_data.append({"ROM": rom, "Tempo": tempo, "Smoothness": smoothness}) # Analyze the rep using Isolation Forest feedback = analyze_single_rep(rep_data[-1], rep_data) # Wireframe color based on form wireframe_color = (0, 255, 0) if stage == "up" or stage == "down" else (0, 0, 255) # Draw wireframe mp_drawing.draw_landmarks( image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS, mp_drawing.DrawingSpec(color=wireframe_color, thickness=5, circle_radius=4), mp_drawing.DrawingSpec(color=wireframe_color, thickness=5, circle_radius=4) ) # Display reps, stage, timer, and feedback draw_text_with_background(image, f"Reps: {counter}", (50, 150), cv2.FONT_HERSHEY_SIMPLEX, 3, (255, 255, 255), 5, (0, 0, 0)) draw_text_with_background(image, f"Stage: {stage if stage else 'N/A'}", (50, 300), cv2.FONT_HERSHEY_SIMPLEX, 3, (255, 255, 255), 5, (0, 0, 0)) draw_text_with_background(image, timer_text, (1000, 50), # Timer in the top-right corner cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 3, (0, 0, 0)) draw_text_with_background(image, feedback, (50, 450), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 3, (0, 0, 0)) # Show video feed cv2.imshow('Workout Feedback', image) # Break if 'q' is pressed if cv2.waitKey(10) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows() # Post-workout analysis analyze_workout_with_isolation_forest(rep_data) if __name__ == "__main__": main() def lateral_raise(): import cv2 import mediapipe as mp import numpy as np import time from sklearn.ensemble import IsolationForest # Mediapipe utilities mp_drawing = mp.solutions.drawing_utils mp_pose = mp.solutions.pose # Function to calculate lateral raise angle def calculate_angle_for_lateral_raise(shoulder, wrist): """ Calculate the angle of the arm relative to the horizontal plane passing through the shoulder. """ horizontal_reference = np.array([1, 0]) # Horizontal vector arm_vector = np.array([wrist[0] - shoulder[0], wrist[1] - shoulder[1]]) dot_product = np.dot(horizontal_reference, arm_vector) magnitude_reference = np.linalg.norm(horizontal_reference) magnitude_arm = np.linalg.norm(arm_vector) if magnitude_arm == 0 or magnitude_reference == 0: return 0 cos_angle = dot_product / (magnitude_reference * magnitude_arm) angle = np.arccos(np.clip(cos_angle, -1.0, 1.0)) return np.degrees(angle) # Function to draw text with a background def draw_text_with_background(image, text, position, font, font_scale, color, thickness, bg_color, padding=10): text_size = cv2.getTextSize(text, font, font_scale, thickness)[0] text_x, text_y = position box_coords = ( (text_x - padding, text_y - padding), (text_x + text_size[0] + padding, text_y + text_size[1] + padding), ) cv2.rectangle(image, box_coords[0], box_coords[1], bg_color, cv2.FILLED) cv2.putText(image, text, (text_x, text_y + text_size[1]), font, font_scale, color, thickness) # Function to check if all required joints are visible def are_key_joints_visible(landmarks, visibility_threshold=0.5): """ Ensure that all required joints are visible based on their visibility scores. """ required_joints = [ mp_pose.PoseLandmark.LEFT_SHOULDER.value, mp_pose.PoseLandmark.RIGHT_SHOULDER.value, mp_pose.PoseLandmark.LEFT_WRIST.value, mp_pose.PoseLandmark.RIGHT_WRIST.value, ] for joint in required_joints: if landmarks[joint].visibility < visibility_threshold: return False return True # Real-time feedback for single rep def analyze_single_rep(rep, rep_data): """Provide actionable feedback for a single rep.""" feedback = [] # Calculate averages from previous reps avg_rom = np.mean([r["ROM"] for r in rep_data]) if rep_data else 0 avg_tempo = np.mean([r["Tempo"] for r in rep_data]) if rep_data else 0 # Dynamic tempo thresholds lower_tempo_threshold = 2.0 # Minimum grace threshold for faster tempo upper_tempo_threshold = 9.0 # Maximum grace threshold for slower tempo # Adjust thresholds after a few reps if len(rep_data) > 3: lower_tempo_threshold = max(2.0, avg_tempo * 0.7) upper_tempo_threshold = min(9.0, avg_tempo * 1.3) # Feedback for ROM if rep["ROM"] < 30: # Minimum ROM threshold feedback.append("Lift arm higher") elif rep_data and rep["ROM"] < avg_rom * 0.8: feedback.append("Increase ROM") # Feedback for Tempo if rep["Tempo"] < lower_tempo_threshold: # Tempo too fast feedback.append("Slow down") elif rep["Tempo"] > upper_tempo_threshold: # Tempo too slow feedback.append("Speed up") return feedback # Post-workout feedback function def analyze_workout_with_isolation_forest(rep_data): if not rep_data: print("No reps completed.") return print("\n--- Post-Workout Summary ---") # Filter valid reps for recalculating thresholds valid_reps = [rep for rep in rep_data if rep["ROM"] > 20] # Ignore very low ROM reps if not valid_reps: print("No valid reps to analyze.") return features = np.array([[rep["ROM"], rep["Tempo"]] for rep in valid_reps]) avg_rom = np.mean(features[:, 0]) avg_tempo = np.mean(features[:, 1]) std_rom = np.std(features[:, 0]) std_tempo = np.std(features[:, 1]) # Adjusted bounds for anomalies rom_lower_bound = max(20, avg_rom - std_rom * 2) tempo_lower_bound = max(1.0, avg_tempo - std_tempo * 2) tempo_upper_bound = min(10.0, avg_tempo + std_tempo * 2) print(f"ROM Lower Bound: {rom_lower_bound}") print(f"Tempo Bounds: {tempo_lower_bound}-{tempo_upper_bound}") # Anomaly detection for i, rep in enumerate(valid_reps, 1): feedback = [] if rep["ROM"] < rom_lower_bound: feedback.append("Low ROM") if rep["Tempo"] < tempo_lower_bound: feedback.append("Too Fast") elif rep["Tempo"] > tempo_upper_bound: feedback.append("Too Slow") if feedback: print(f"Rep {i}: Anomalous | Feedback: {', '.join(feedback[:1])}") # Use Isolation Forest for secondary anomaly detection model = IsolationForest(contamination=0.1, random_state=42) # Reduced contamination predictions = model.fit_predict(features) for i, prediction in enumerate(predictions, 1): if prediction == -1: # Outlier print(f"Rep {i}: Isolation Forest flagged this rep as anomalous.") # Main workout tracking function def main(): cap = cv2.VideoCapture(0) counter = 0 # Rep counter stage = None # Movement stage feedback = [] # Real-time feedback for the video feed rep_data = [] # Store metrics for each rep angles_during_rep = [] # Track angles during a single rep workout_start_time = None # Timer start with mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5) as pose: while cap.isOpened(): ret, frame = cap.read() if not ret: print("Failed to grab frame.") break # Initialize workout start time if workout_start_time is None: workout_start_time = time.time() # Timer elapsed_time = time.time() - workout_start_time timer_text = f"Timer: {int(elapsed_time)}s" # Convert the image to RGB image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) image.flags.writeable = False results = pose.process(image) # Convert back to BGR image.flags.writeable = True image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) # Check if pose landmarks are detected if results.pose_landmarks: landmarks = results.pose_landmarks.landmark # Check if key joints are visible if not are_key_joints_visible(landmarks): draw_text_with_background( image, "Ensure all joints are visible", (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, (0, 0, 255) ) cv2.imshow("Lateral Raise Tracker", image) continue # Extract key joints left_shoulder = [ landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x, landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y, ] left_wrist = [ landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x, landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y, ] # Calculate angle for lateral raise angle = calculate_angle_for_lateral_raise(left_shoulder, left_wrist) # Track angles during a rep if stage == "up" or stage == "down": angles_during_rep.append(angle) # Stage logic for counting reps if angle < 20 and stage != "down": stage = "down" if counter == 10: # Stop on the down stage of the 10th rep print("Workout complete! 10 reps reached.") break # Calculate ROM for the completed rep if len(angles_during_rep) > 1: rom = max(angles_during_rep) - min(angles_during_rep) else: rom = 0.0 tempo = elapsed_time print(f"Rep {counter + 1}: ROM={rom:.2f}, Tempo={tempo:.2f}s") # Record metrics for the rep rep_data.append({ "ROM": rom, "Tempo": tempo, }) # Reset angles and timer for the next rep angles_during_rep = [] workout_start_time = time.time() # Reset timer if 70 <= angle <= 110 and stage == "down": stage = "up" counter += 1 # Analyze feedback feedback = analyze_single_rep(rep_data[-1], rep_data) # Determine wireframe color wireframe_color = (0, 255, 0) if not feedback else (0, 0, 255) # Display feedback draw_text_with_background(image, f"Reps: {counter}", (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, (0, 0, 0)) draw_text_with_background(image, " | ".join(feedback), (50, 120), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, (0, 0, 0)) draw_text_with_background(image, timer_text, (50, 190), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, (0, 0, 0)) # Render detections with wireframe color mp_drawing.draw_landmarks( image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS, mp_drawing.DrawingSpec(color=wireframe_color, thickness=2, circle_radius=2), mp_drawing.DrawingSpec(color=wireframe_color, thickness=2, circle_radius=2), ) # Display the image cv2.imshow("Lateral Raise Tracker", image) if cv2.waitKey(10) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows() # Post-workout analysis analyze_workout_with_isolation_forest(rep_data) if __name__ == "__main__": main() def shoulder_press(): import cv2 import mediapipe as mp import numpy as np import time # Mediapipe utilities mp_drawing = mp.solutions.drawing_utils mp_pose = mp.solutions.pose # Function to calculate angles def calculate_angle(point_a, point_b, point_c): vector_ab = np.array([point_a[0] - point_b[0], point_a[1] - point_b[1]]) vector_cb = np.array([point_c[0] - point_b[0], point_c[1] - point_b[1]]) dot_product = np.dot(vector_ab, vector_cb) magnitude_ab = np.linalg.norm(vector_ab) magnitude_cb = np.linalg.norm(vector_cb) if magnitude_ab == 0 or magnitude_cb == 0: return 0 cos_angle = dot_product / (magnitude_ab * magnitude_cb) angle = np.arccos(np.clip(cos_angle, -1.0, 1.0)) return np.degrees(angle) # Function to check if all required joints are visible def are_key_joints_visible(landmarks, visibility_threshold=0.5): required_joints = [ mp_pose.PoseLandmark.LEFT_SHOULDER.value, mp_pose.PoseLandmark.RIGHT_SHOULDER.value, mp_pose.PoseLandmark.LEFT_ELBOW.value, mp_pose.PoseLandmark.RIGHT_ELBOW.value, mp_pose.PoseLandmark.LEFT_WRIST.value, mp_pose.PoseLandmark.RIGHT_WRIST.value, ] for joint in required_joints: if landmarks[joint].visibility < visibility_threshold: return False return True # Function to draw text with a background def draw_text_with_background(image, text, position, font, font_scale, color, thickness, bg_color, padding=10): text_size = cv2.getTextSize(text, font, font_scale, thickness)[0] text_x, text_y = position box_coords = ( (text_x - padding, text_y - padding), (text_x + text_size[0] + padding, text_y + text_size[1] + padding), ) cv2.rectangle(image, box_coords[0], box_coords[1], bg_color, cv2.FILLED) cv2.putText(image, text, (text_x, text_y + text_size[1]), font, font_scale, color, thickness) # Main workout tracking function def main(): cap = cv2.VideoCapture(0) counter = 0 stage = None feedback = "" workout_start_time = None rep_start_time = None with mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5) as pose: while cap.isOpened(): ret, frame = cap.read() if not ret: print("Failed to grab frame.") break # Initialize workout start time if workout_start_time is None: workout_start_time = time.time() # Timer elapsed_time = time.time() - workout_start_time timer_text = f"Timer: {int(elapsed_time)}s" # Convert the image to RGB image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) image.flags.writeable = False results = pose.process(image) # Convert back to BGR image.flags.writeable = True image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) # Check if pose landmarks are detected if results.pose_landmarks: landmarks = results.pose_landmarks.landmark # Check if key joints are visible if not are_key_joints_visible(landmarks): feedback = "Ensure all joints are visible" draw_text_with_background( image, feedback, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, (0, 0, 255) ) cv2.imshow("Shoulder Press Tracker", image) continue # Extract key joints for both arms left_shoulder = [ landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x, landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y, ] left_elbow = [ landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x, landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].y, ] left_wrist = [ landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x, landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y, ] right_shoulder = [ landmarks[mp_pose.PoseLandmark.RIGHT_SHOULDER.value].x, landmarks[mp_pose.PoseLandmark.RIGHT_SHOULDER.value].y, ] right_elbow = [ landmarks[mp_pose.PoseLandmark.RIGHT_ELBOW.value].x, landmarks[mp_pose.PoseLandmark.RIGHT_ELBOW.value].y, ] right_wrist = [ landmarks[mp_pose.PoseLandmark.RIGHT_WRIST.value].x, landmarks[mp_pose.PoseLandmark.RIGHT_WRIST.value].y, ] # Calculate angles left_elbow_angle = calculate_angle(left_shoulder, left_elbow, left_wrist) right_elbow_angle = calculate_angle(right_shoulder, right_elbow, right_wrist) # Check starting and ending positions if 80 <= left_elbow_angle <= 100 and 80 <= right_elbow_angle <= 100 and stage != "down": stage = "down" if counter == 10: feedback = "Workout complete! 10 reps done." draw_text_with_background(image, feedback, (50, 120), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, (0, 0, 255)) cv2.imshow("Shoulder Press Tracker", image) break if rep_start_time is not None: tempo = time.time() - rep_start_time feedback = f"Rep {counter} completed! Tempo: {tempo:.2f}s" rep_start_time = None elif left_elbow_angle > 160 and right_elbow_angle > 160 and stage == "down": stage = "up" counter += 1 rep_start_time = time.time() # Wireframe color wireframe_color = (0, 255, 0) if "completed" in feedback or "Good" in feedback else (0, 0, 255) # Display feedback draw_text_with_background(image, f"Reps: {counter}", (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, (0, 0, 0)) draw_text_with_background(image, feedback, (50, 120), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, (0, 0, 0)) draw_text_with_background(image, timer_text, (50, 190), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255, 255, 255), 2, (0, 0, 0)) # Render detections with wireframe color mp_drawing.draw_landmarks( image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS, mp_drawing.DrawingSpec(color=wireframe_color, thickness=2, circle_radius=2), mp_drawing.DrawingSpec(color=wireframe_color, thickness=2, circle_radius=2), ) # Display the image cv2.imshow("Shoulder Press Tracker", image) if cv2.waitKey(10) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows() if __name__ == "__main__": main() # Streamlit configuration st.set_page_config(page_title="Workout Tracker", page_icon="💪", layout="centered") # Custom CSS for styling st.markdown( ''' ''', unsafe_allow_html=True ) # Title and Introduction st.title("Workout Tracker") st.markdown("Welcome to the **Workout Tracker App**! Select your desired workout below and receive real-time feedback as you exercise.") # Check webcam availability def check_webcam(): try: cap = cv2.VideoCapture(0) if not cap.isOpened(): st.error("Webcam not detected! Please ensure a webcam is connected.") return False cap.release() return True except Exception as e: st.error(f"Error accessing webcam: {e}") return False # Workout Selection workout_option = st.radio("Select Your Workout:", ["Bicep Curl", "Lateral Raise", "Shoulder Press"]) # Start Button if st.button("Start Workout"): if not check_webcam(): st.stop() if workout_option == "Bicep Curl": st.write("Launching Bicep Curl Tracker...") bicep_curl() elif workout_option == "Lateral Raise": st.write("Launching Lateral Raise Tracker...") lateral_raise() elif workout_option == "Shoulder Press": st.write("Launching Shoulder Press Tracker...") shoulder_press() # Footer st.markdown("---") st.markdown("**Note**: Close the workout window or press 'q' in the camera feed to stop the workout.")