Upload 67 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +3 -0
- LICENSE +21 -0
- README.md +83 -10
- app.py +314 -0
- create_static_folders.py +63 -0
- db/__pycache__/workout_logger.cpython-311.pyc +0 -0
- db/__pycache__/workout_logger.cpython-312.pyc +0 -0
- db/__pycache__/workout_logger.cpython-39.pyc +0 -0
- db/workout_logger.py +0 -0
- exercises/__pycache__/hammer_curl.cpython-311.pyc +0 -0
- exercises/__pycache__/hammer_curl.cpython-312.pyc +0 -0
- exercises/__pycache__/hammer_curl.cpython-39.pyc +0 -0
- exercises/__pycache__/push_up.cpython-311.pyc +0 -0
- exercises/__pycache__/push_up.cpython-312.pyc +0 -0
- exercises/__pycache__/push_up.cpython-39.pyc +0 -0
- exercises/__pycache__/squat.cpython-311.pyc +0 -0
- exercises/__pycache__/squat.cpython-312.pyc +0 -0
- exercises/__pycache__/squat.cpython-39.pyc +0 -0
- exercises/hammer_curl.py +114 -0
- exercises/push_up.py +77 -0
- exercises/squat.py +62 -0
- feedback/__pycache__/indicators.cpython-311.pyc +0 -0
- feedback/__pycache__/indicators.cpython-312.pyc +0 -0
- feedback/__pycache__/indicators.cpython-39.pyc +0 -0
- feedback/__pycache__/information.cpython-311.pyc +0 -0
- feedback/__pycache__/information.cpython-312.pyc +0 -0
- feedback/__pycache__/information.cpython-39.pyc +0 -0
- feedback/__pycache__/layout.cpython-311.pyc +0 -0
- feedback/__pycache__/layout.cpython-312.pyc +0 -0
- feedback/__pycache__/layout.cpython-39.pyc +0 -0
- feedback/indicators.py +50 -0
- feedback/information.py +41 -0
- feedback/layout.py +16 -0
- main.py +82 -0
- output/images/Screenshot 2024-09-08 030742.png +3 -0
- output/images/Screenshot 2024-09-08 030816.png +3 -0
- output/images/Screenshot 2024-09-08 030836.png +3 -0
- pose_estimation/__pycache__/angle_calculation.cpython-311.pyc +0 -0
- pose_estimation/__pycache__/angle_calculation.cpython-312.pyc +0 -0
- pose_estimation/__pycache__/angle_calculation.cpython-39.pyc +0 -0
- pose_estimation/__pycache__/estimation.cpython-311.pyc +0 -0
- pose_estimation/__pycache__/estimation.cpython-312.pyc +0 -0
- pose_estimation/__pycache__/estimation.cpython-39.pyc +0 -0
- pose_estimation/angle_calculation.py +23 -0
- pose_estimation/estimation.py +81 -0
- requirements.txt +0 -0
- static/css/dashboard.css +138 -0
- static/css/style.css +210 -0
- static/images/README.txt +7 -0
- static/images/hammer_curl.png +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
output/images/Screenshot[[:space:]]2024-09-08[[:space:]]030742.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
output/images/Screenshot[[:space:]]2024-09-08[[:space:]]030816.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
output/images/Screenshot[[:space:]]2024-09-08[[:space:]]030836.png filter=lfs diff=lfs merge=lfs -text
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024 Yakup Zengin
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -1,10 +1,83 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Fitness Trainer with Pose Estimation
|
| 2 |
+
|
| 3 |
+
An AI-powered web application that tracks exercises using computer vision and provides real-time feedback.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- Real-time pose estimation using MediaPipe
|
| 8 |
+
- Multiple exercise types: Squats, Push-ups, and Hammer Curls
|
| 9 |
+
- Customizable sets and repetitions
|
| 10 |
+
- Exercise form feedback
|
| 11 |
+
- Progress tracking
|
| 12 |
+
- Web interface for easy access
|
| 13 |
+
|
| 14 |
+
## Installation
|
| 15 |
+
|
| 16 |
+
1. Clone the repository:
|
| 17 |
+
|
| 18 |
+
```
|
| 19 |
+
git clone https://github.com/yourusername/fitness-trainer-pose-estimation.git
|
| 20 |
+
cd fitness-trainer-pose-estimation
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
2. Install dependencies:
|
| 24 |
+
|
| 25 |
+
```
|
| 26 |
+
pip install -r requirements.txt
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
3. Set up the static folder structure:
|
| 30 |
+
|
| 31 |
+
```
|
| 32 |
+
mkdir -p static/images
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
4. Add exercise images to the static/images folder:
|
| 36 |
+
- squat.png
|
| 37 |
+
- push_up.png
|
| 38 |
+
- hammer_curl.png
|
| 39 |
+
|
| 40 |
+
## Usage
|
| 41 |
+
|
| 42 |
+
1. Start the Flask server:
|
| 43 |
+
|
| 44 |
+
```
|
| 45 |
+
python app.py
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
2. Open a web browser and navigate to:
|
| 49 |
+
|
| 50 |
+
```
|
| 51 |
+
http://127.0.0.1:5000
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
3. Select an exercise type, set your desired number of repetitions and sets, then click "Start Workout"
|
| 55 |
+
|
| 56 |
+
4. Position yourself in front of your camera so that your full body is visible
|
| 57 |
+
|
| 58 |
+
5. Follow the on-screen guidance to perform the exercise correctly
|
| 59 |
+
|
| 60 |
+
## Project Structure
|
| 61 |
+
|
| 62 |
+
- `app.py` - Main Flask application
|
| 63 |
+
- `templates/` - HTML templates
|
| 64 |
+
- `static/` - CSS, JavaScript, and images
|
| 65 |
+
- `pose_estimation/` - Pose estimation modules
|
| 66 |
+
- `exercises/` - Exercise tracking classes
|
| 67 |
+
- `feedback/` - User feedback modules
|
| 68 |
+
- `utils/` - Helper functions and utilities
|
| 69 |
+
|
| 70 |
+
## Technologies Used
|
| 71 |
+
|
| 72 |
+
- Flask - Web framework
|
| 73 |
+
- OpenCV - Computer vision
|
| 74 |
+
- MediaPipe - Pose estimation
|
| 75 |
+
- HTML/CSS/JavaScript - Frontend
|
| 76 |
+
|
| 77 |
+
## Contributing
|
| 78 |
+
|
| 79 |
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
| 80 |
+
|
| 81 |
+
## License
|
| 82 |
+
|
| 83 |
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
app.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, Response, request, jsonify, session, redirect, url_for
|
| 2 |
+
import cv2
|
| 3 |
+
import threading
|
| 4 |
+
import time
|
| 5 |
+
import sys
|
| 6 |
+
import traceback
|
| 7 |
+
import logging
|
| 8 |
+
from flask_cors import CORS
|
| 9 |
+
|
| 10 |
+
# Set up logging
|
| 11 |
+
logging.basicConfig(level=logging.DEBUG,
|
| 12 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 13 |
+
handlers=[logging.StreamHandler()])
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
# Import attempt with error handling
|
| 17 |
+
try:
|
| 18 |
+
from pose_estimation.estimation import PoseEstimator
|
| 19 |
+
from exercises.squat import Squat
|
| 20 |
+
from exercises.hammer_curl import HammerCurl
|
| 21 |
+
from exercises.push_up import PushUp
|
| 22 |
+
from feedback.information import get_exercise_info
|
| 23 |
+
from feedback.layout import layout_indicators
|
| 24 |
+
from utils.draw_text_with_background import draw_text_with_background
|
| 25 |
+
logger.info("Successfully imported pose estimation modules")
|
| 26 |
+
except ImportError as e:
|
| 27 |
+
logger.error(f"Failed to import required modules: {e}")
|
| 28 |
+
traceback.print_exc()
|
| 29 |
+
sys.exit(1)
|
| 30 |
+
|
| 31 |
+
# Try to import WorkoutLogger with fallback
|
| 32 |
+
try:
|
| 33 |
+
from db.workout_logger import WorkoutLogger
|
| 34 |
+
workout_logger = WorkoutLogger()
|
| 35 |
+
logger.info("Successfully initialized workout logger")
|
| 36 |
+
except ImportError:
|
| 37 |
+
logger.warning("WorkoutLogger import failed, creating dummy class")
|
| 38 |
+
|
| 39 |
+
class DummyWorkoutLogger:
|
| 40 |
+
def __init__(self):
|
| 41 |
+
pass
|
| 42 |
+
def log_workout(self, *args, **kwargs):
|
| 43 |
+
return {}
|
| 44 |
+
def get_recent_workouts(self, *args, **kwargs):
|
| 45 |
+
return []
|
| 46 |
+
def get_weekly_stats(self, *args, **kwargs):
|
| 47 |
+
return {}
|
| 48 |
+
def get_exercise_distribution(self, *args, **kwargs):
|
| 49 |
+
return {}
|
| 50 |
+
def get_user_stats(self, *args, **kwargs):
|
| 51 |
+
return {'total_workouts': 0, 'total_exercises': 0, 'streak_days': 0}
|
| 52 |
+
|
| 53 |
+
workout_logger = DummyWorkoutLogger()
|
| 54 |
+
|
| 55 |
+
logger.info("Setting up Flask application")
|
| 56 |
+
app = Flask(__name__)
|
| 57 |
+
app.secret_key = 'fitness_trainer_secret_key' # Required for sessions
|
| 58 |
+
CORS(app)
|
| 59 |
+
# Global variables
|
| 60 |
+
camera = None
|
| 61 |
+
output_frame = None
|
| 62 |
+
lock = threading.Lock()
|
| 63 |
+
exercise_running = False
|
| 64 |
+
current_exercise = None
|
| 65 |
+
current_exercise_data = None
|
| 66 |
+
exercise_counter = 0
|
| 67 |
+
exercise_goal = 0
|
| 68 |
+
sets_completed = 0
|
| 69 |
+
sets_goal = 0
|
| 70 |
+
workout_start_time = None
|
| 71 |
+
|
| 72 |
+
def initialize_camera():
|
| 73 |
+
global camera
|
| 74 |
+
if camera is None:
|
| 75 |
+
camera = cv2.VideoCapture(0)
|
| 76 |
+
return camera
|
| 77 |
+
|
| 78 |
+
def release_camera():
|
| 79 |
+
global camera
|
| 80 |
+
if camera is not None:
|
| 81 |
+
camera.release()
|
| 82 |
+
camera = None
|
| 83 |
+
|
| 84 |
+
def generate_frames():
|
| 85 |
+
global output_frame, lock, exercise_running, current_exercise, current_exercise_data
|
| 86 |
+
global exercise_counter, exercise_goal, sets_completed, sets_goal
|
| 87 |
+
|
| 88 |
+
pose_estimator = PoseEstimator()
|
| 89 |
+
|
| 90 |
+
while True:
|
| 91 |
+
if camera is None:
|
| 92 |
+
continue
|
| 93 |
+
|
| 94 |
+
success, frame = camera.read()
|
| 95 |
+
if not success:
|
| 96 |
+
continue
|
| 97 |
+
|
| 98 |
+
# Only process frames if an exercise is running
|
| 99 |
+
if exercise_running and current_exercise:
|
| 100 |
+
# Process with pose estimation
|
| 101 |
+
results = pose_estimator.estimate_pose(frame, current_exercise_data['type'])
|
| 102 |
+
|
| 103 |
+
if results.pose_landmarks:
|
| 104 |
+
# Track exercise based on type
|
| 105 |
+
if current_exercise_data['type'] == "squat":
|
| 106 |
+
counter, angle, stage = current_exercise.track_squat(results.pose_landmarks.landmark, frame)
|
| 107 |
+
layout_indicators(frame, current_exercise_data['type'], (counter, angle, stage))
|
| 108 |
+
exercise_counter = counter
|
| 109 |
+
|
| 110 |
+
elif current_exercise_data['type'] == "push_up":
|
| 111 |
+
counter, angle, stage = current_exercise.track_push_up(results.pose_landmarks.landmark, frame)
|
| 112 |
+
layout_indicators(frame, current_exercise_data['type'], (counter, angle, stage))
|
| 113 |
+
exercise_counter = counter
|
| 114 |
+
|
| 115 |
+
elif current_exercise_data['type'] == "hammer_curl":
|
| 116 |
+
(counter_right, angle_right, counter_left, angle_left,
|
| 117 |
+
warning_message_right, warning_message_left, progress_right,
|
| 118 |
+
progress_left, stage_right, stage_left) = current_exercise.track_hammer_curl(
|
| 119 |
+
results.pose_landmarks.landmark, frame)
|
| 120 |
+
layout_indicators(frame, current_exercise_data['type'],
|
| 121 |
+
(counter_right, angle_right, counter_left, angle_left,
|
| 122 |
+
warning_message_right, warning_message_left,
|
| 123 |
+
progress_right, progress_left, stage_right, stage_left))
|
| 124 |
+
exercise_counter = max(counter_right, counter_left)
|
| 125 |
+
|
| 126 |
+
# Display exercise information
|
| 127 |
+
exercise_info = get_exercise_info(current_exercise_data['type'])
|
| 128 |
+
draw_text_with_background(frame, f"Exercise: {exercise_info.get('name', 'N/A')}", (40, 50),
|
| 129 |
+
cv2.FONT_HERSHEY_DUPLEX, 0.7, (255, 255, 255), (118, 29, 14), 1)
|
| 130 |
+
draw_text_with_background(frame, f"Reps Goal: {exercise_goal}", (40, 80),
|
| 131 |
+
cv2.FONT_HERSHEY_DUPLEX, 0.7, (255, 255, 255), (118, 29, 14), 1)
|
| 132 |
+
draw_text_with_background(frame, f"Sets Goal: {sets_goal}", (40, 110),
|
| 133 |
+
cv2.FONT_HERSHEY_DUPLEX, 0.7, (255, 255, 255), (118, 29, 14), 1)
|
| 134 |
+
draw_text_with_background(frame, f"Current Set: {sets_completed + 1}", (40, 140),
|
| 135 |
+
cv2.FONT_HERSHEY_DUPLEX, 0.7, (255, 255, 255), (118, 29, 14), 1)
|
| 136 |
+
|
| 137 |
+
# Check if rep goal is reached for current set
|
| 138 |
+
if exercise_counter >= exercise_goal:
|
| 139 |
+
sets_completed += 1
|
| 140 |
+
exercise_counter = 0
|
| 141 |
+
# Reset exercise counter in the appropriate exercise object
|
| 142 |
+
if current_exercise_data['type'] == "squat" or current_exercise_data['type'] == "push_up":
|
| 143 |
+
current_exercise.counter = 0
|
| 144 |
+
elif current_exercise_data['type'] == "hammer_curl":
|
| 145 |
+
current_exercise.counter_right = 0
|
| 146 |
+
current_exercise.counter_left = 0
|
| 147 |
+
|
| 148 |
+
# Check if all sets are completed
|
| 149 |
+
if sets_completed >= sets_goal:
|
| 150 |
+
exercise_running = False
|
| 151 |
+
draw_text_with_background(frame, "WORKOUT COMPLETE!", (frame.shape[1]//2 - 150, frame.shape[0]//2),
|
| 152 |
+
cv2.FONT_HERSHEY_DUPLEX, 1.2, (255, 255, 255), (0, 200, 0), 2)
|
| 153 |
+
else:
|
| 154 |
+
draw_text_with_background(frame, f"SET {sets_completed} COMPLETE! Rest for 30 sec",
|
| 155 |
+
(frame.shape[1]//2 - 200, frame.shape[0]//2),
|
| 156 |
+
cv2.FONT_HERSHEY_DUPLEX, 1.0, (255, 255, 255), (0, 0, 200), 2)
|
| 157 |
+
# We could add rest timer functionality here
|
| 158 |
+
else:
|
| 159 |
+
# Display welcome message if no exercise is running
|
| 160 |
+
cv2.putText(frame, "Select an exercise to begin", (frame.shape[1]//2 - 150, frame.shape[0]//2),
|
| 161 |
+
cv2.FONT_HERSHEY_DUPLEX, 0.8, (255, 255, 255), 1)
|
| 162 |
+
|
| 163 |
+
# Encode the frame in JPEG format
|
| 164 |
+
with lock:
|
| 165 |
+
output_frame = frame.copy()
|
| 166 |
+
|
| 167 |
+
# Yield the frame in byte format
|
| 168 |
+
ret, buffer = cv2.imencode('.jpg', output_frame)
|
| 169 |
+
frame = buffer.tobytes()
|
| 170 |
+
yield (b'--frame\r\n'
|
| 171 |
+
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
|
| 172 |
+
|
| 173 |
+
@app.route('/')
|
| 174 |
+
def index():
|
| 175 |
+
"""Home page with exercise selection"""
|
| 176 |
+
logger.info("Rendering index page")
|
| 177 |
+
try:
|
| 178 |
+
return render_template('index.html')
|
| 179 |
+
except Exception as e:
|
| 180 |
+
logger.error(f"Error rendering index: {e}")
|
| 181 |
+
return f"Error rendering template: {str(e)}", 500
|
| 182 |
+
|
| 183 |
+
@app.route('/dashboard')
|
| 184 |
+
def dashboard():
|
| 185 |
+
"""Dashboard page with workout statistics"""
|
| 186 |
+
logger.info("Rendering dashboard page")
|
| 187 |
+
try:
|
| 188 |
+
# Get data for the dashboard
|
| 189 |
+
recent_workouts = workout_logger.get_recent_workouts(5)
|
| 190 |
+
weekly_stats = workout_logger.get_weekly_stats()
|
| 191 |
+
exercise_distribution = workout_logger.get_exercise_distribution()
|
| 192 |
+
user_stats = workout_logger.get_user_stats()
|
| 193 |
+
|
| 194 |
+
# Format workouts for display
|
| 195 |
+
formatted_workouts = []
|
| 196 |
+
for workout in recent_workouts:
|
| 197 |
+
formatted_workouts.append({
|
| 198 |
+
'date': workout['date'],
|
| 199 |
+
'exercise': workout['exercise_type'].replace('_', ' ').title(),
|
| 200 |
+
'sets': workout['sets'],
|
| 201 |
+
'reps': workout['reps'],
|
| 202 |
+
'duration': f"{workout['duration_seconds'] // 60}:{workout['duration_seconds'] % 60:02d}"
|
| 203 |
+
})
|
| 204 |
+
|
| 205 |
+
# Calculate total workouts this week
|
| 206 |
+
weekly_workout_count = sum(day['workout_count'] for day in weekly_stats.values())
|
| 207 |
+
|
| 208 |
+
return render_template('dashboard.html',
|
| 209 |
+
recent_workouts=formatted_workouts,
|
| 210 |
+
weekly_workouts=weekly_workout_count,
|
| 211 |
+
total_workouts=user_stats['total_workouts'],
|
| 212 |
+
total_exercises=user_stats['total_exercises'],
|
| 213 |
+
streak_days=user_stats['streak_days'])
|
| 214 |
+
except Exception as e:
|
| 215 |
+
logger.error(f"Error in dashboard: {e}")
|
| 216 |
+
traceback.print_exc()
|
| 217 |
+
return f"Error loading dashboard: {str(e)}", 500
|
| 218 |
+
|
| 219 |
+
@app.route('/video_feed')
|
| 220 |
+
def video_feed():
|
| 221 |
+
"""Video streaming route"""
|
| 222 |
+
return Response(generate_frames(),
|
| 223 |
+
mimetype='multipart/x-mixed-replace; boundary=frame')
|
| 224 |
+
|
| 225 |
+
@app.route('/start_exercise', methods=['POST'])
|
| 226 |
+
def start_exercise():
|
| 227 |
+
"""Start a new exercise based on user selection"""
|
| 228 |
+
global exercise_running, current_exercise, current_exercise_data
|
| 229 |
+
global exercise_counter, exercise_goal, sets_completed, sets_goal
|
| 230 |
+
global workout_start_time
|
| 231 |
+
|
| 232 |
+
data = request.json
|
| 233 |
+
exercise_type = data.get('exercise_type')
|
| 234 |
+
sets_goal = int(data.get('sets', 3))
|
| 235 |
+
exercise_goal = int(data.get('reps', 10))
|
| 236 |
+
|
| 237 |
+
# Initialize camera if not already done
|
| 238 |
+
initialize_camera()
|
| 239 |
+
|
| 240 |
+
# Reset counters
|
| 241 |
+
exercise_counter = 0
|
| 242 |
+
sets_completed = 0
|
| 243 |
+
workout_start_time = time.time()
|
| 244 |
+
|
| 245 |
+
# Initialize the appropriate exercise class
|
| 246 |
+
if exercise_type == "squat":
|
| 247 |
+
current_exercise = Squat()
|
| 248 |
+
elif exercise_type == "push_up":
|
| 249 |
+
current_exercise = PushUp()
|
| 250 |
+
elif exercise_type == "hammer_curl":
|
| 251 |
+
current_exercise = HammerCurl()
|
| 252 |
+
else:
|
| 253 |
+
return jsonify({'success': False, 'error': 'Invalid exercise type'})
|
| 254 |
+
|
| 255 |
+
# Store exercise data
|
| 256 |
+
current_exercise_data = {
|
| 257 |
+
'type': exercise_type,
|
| 258 |
+
'sets': sets_goal,
|
| 259 |
+
'reps': exercise_goal
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
# Start the exercise
|
| 263 |
+
exercise_running = True
|
| 264 |
+
|
| 265 |
+
return jsonify({'success': True})
|
| 266 |
+
|
| 267 |
+
@app.route('/stop_exercise', methods=['POST'])
|
| 268 |
+
def stop_exercise():
|
| 269 |
+
"""Stop the current exercise and log the workout"""
|
| 270 |
+
global exercise_running, current_exercise_data, workout_start_time
|
| 271 |
+
global exercise_counter, exercise_goal, sets_completed, sets_goal
|
| 272 |
+
|
| 273 |
+
if exercise_running and current_exercise_data:
|
| 274 |
+
# Calculate duration
|
| 275 |
+
duration = int(time.time() - workout_start_time) if workout_start_time else 0
|
| 276 |
+
|
| 277 |
+
# Log the workout
|
| 278 |
+
workout_logger.log_workout(
|
| 279 |
+
exercise_type=current_exercise_data['type'],
|
| 280 |
+
sets=sets_completed + (1 if exercise_counter > 0 else 0), # Include partial set
|
| 281 |
+
reps=exercise_goal,
|
| 282 |
+
duration_seconds=duration
|
| 283 |
+
)
|
| 284 |
+
release_camera()
|
| 285 |
+
exercise_running = False
|
| 286 |
+
return jsonify({'success': True})
|
| 287 |
+
|
| 288 |
+
@app.route('/get_status', methods=['GET'])
|
| 289 |
+
def get_status():
|
| 290 |
+
"""Return current exercise status"""
|
| 291 |
+
global exercise_counter, sets_completed, exercise_goal, sets_goal, exercise_running
|
| 292 |
+
|
| 293 |
+
return jsonify({
|
| 294 |
+
'exercise_running': exercise_running,
|
| 295 |
+
'current_reps': exercise_counter,
|
| 296 |
+
'current_set': sets_completed + 1 if exercise_running else 0,
|
| 297 |
+
'total_sets': sets_goal,
|
| 298 |
+
'rep_goal': exercise_goal
|
| 299 |
+
})
|
| 300 |
+
|
| 301 |
+
@app.route('/profile')
|
| 302 |
+
def profile():
|
| 303 |
+
"""User profile page - placeholder for future development"""
|
| 304 |
+
return "Profile page - Coming soon!"
|
| 305 |
+
|
| 306 |
+
if __name__ == '__main__':
|
| 307 |
+
try:
|
| 308 |
+
logger.info("Starting the Flask application on http://127.0.0.1:5000")
|
| 309 |
+
print("Starting Fitness Trainer app, please wait...")
|
| 310 |
+
print("Open http://127.0.0.1:5000 in your web browser when the server starts")
|
| 311 |
+
app.run(debug=True)
|
| 312 |
+
except Exception as e:
|
| 313 |
+
logger.error(f"Failed to start application: {e}")
|
| 314 |
+
traceback.print_exc()
|
create_static_folders.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import shutil
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
def create_directory_structure():
|
| 6 |
+
"""Create the necessary directory structure for static files."""
|
| 7 |
+
base_dir = Path(__file__).parent
|
| 8 |
+
|
| 9 |
+
# Create static directories
|
| 10 |
+
static_dir = base_dir / 'static'
|
| 11 |
+
css_dir = static_dir / 'css'
|
| 12 |
+
js_dir = static_dir / 'js'
|
| 13 |
+
images_dir = static_dir / 'images'
|
| 14 |
+
|
| 15 |
+
# Create directories if they don't exist
|
| 16 |
+
for directory in [static_dir, css_dir, js_dir, images_dir]:
|
| 17 |
+
directory.mkdir(exist_ok=True)
|
| 18 |
+
print(f"Created directory: {directory}")
|
| 19 |
+
|
| 20 |
+
# Create sample images if they don't exist
|
| 21 |
+
create_placeholder_image(images_dir / 'squat.png', "Squat")
|
| 22 |
+
create_placeholder_image(images_dir / 'push_up.png', "Push Up")
|
| 23 |
+
create_placeholder_image(images_dir / 'hammer_curl.png', "Hammer Curl")
|
| 24 |
+
|
| 25 |
+
print("\nDirectory structure created successfully!")
|
| 26 |
+
print(f"Static files should be placed in: {static_dir}")
|
| 27 |
+
print("Make sure the following files exist:")
|
| 28 |
+
print(f" - {images_dir / 'squat.png'}")
|
| 29 |
+
print(f" - {images_dir / 'push_up.png'}")
|
| 30 |
+
print(f" - {images_dir / 'hammer_curl.png'}")
|
| 31 |
+
|
| 32 |
+
def create_placeholder_image(filepath, text="Exercise"):
|
| 33 |
+
"""Create a simple placeholder image using PIL."""
|
| 34 |
+
try:
|
| 35 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 36 |
+
|
| 37 |
+
# Create a blank image with a colored background
|
| 38 |
+
img = Image.new('RGB', (200, 200), color=(73, 109, 137))
|
| 39 |
+
d = ImageDraw.Draw(img)
|
| 40 |
+
|
| 41 |
+
# Try to use a default font
|
| 42 |
+
try:
|
| 43 |
+
font = ImageFont.truetype("arial.ttf", 18)
|
| 44 |
+
except IOError:
|
| 45 |
+
font = ImageFont.load_default()
|
| 46 |
+
|
| 47 |
+
# Draw text in the center of the image
|
| 48 |
+
d.text((100, 100), text, fill=(255, 255, 255), anchor="mm", font=font)
|
| 49 |
+
|
| 50 |
+
# Save the image
|
| 51 |
+
img.save(filepath)
|
| 52 |
+
print(f"Created placeholder image: {filepath}")
|
| 53 |
+
|
| 54 |
+
except ImportError:
|
| 55 |
+
# If PIL is not available, create an empty file
|
| 56 |
+
print("PIL not installed. Installing empty image files.")
|
| 57 |
+
with open(filepath, 'wb') as f:
|
| 58 |
+
f.write(b'')
|
| 59 |
+
print(f"Created empty file: {filepath}")
|
| 60 |
+
|
| 61 |
+
if __name__ == "__main__":
|
| 62 |
+
create_directory_structure()
|
| 63 |
+
print("\nRun 'pip install pillow' if you want to generate proper placeholder images.")
|
db/__pycache__/workout_logger.cpython-311.pyc
ADDED
|
Binary file (221 Bytes). View file
|
|
|
db/__pycache__/workout_logger.cpython-312.pyc
ADDED
|
Binary file (170 Bytes). View file
|
|
|
db/__pycache__/workout_logger.cpython-39.pyc
ADDED
|
Binary file (188 Bytes). View file
|
|
|
db/workout_logger.py
ADDED
|
File without changes
|
exercises/__pycache__/hammer_curl.cpython-311.pyc
ADDED
|
Binary file (7.86 kB). View file
|
|
|
exercises/__pycache__/hammer_curl.cpython-312.pyc
ADDED
|
Binary file (7.52 kB). View file
|
|
|
exercises/__pycache__/hammer_curl.cpython-39.pyc
ADDED
|
Binary file (3.8 kB). View file
|
|
|
exercises/__pycache__/push_up.cpython-311.pyc
ADDED
|
Binary file (5.83 kB). View file
|
|
|
exercises/__pycache__/push_up.cpython-312.pyc
ADDED
|
Binary file (5.49 kB). View file
|
|
|
exercises/__pycache__/push_up.cpython-39.pyc
ADDED
|
Binary file (2.86 kB). View file
|
|
|
exercises/__pycache__/squat.cpython-311.pyc
ADDED
|
Binary file (5.22 kB). View file
|
|
|
exercises/__pycache__/squat.cpython-312.pyc
ADDED
|
Binary file (4.84 kB). View file
|
|
|
exercises/__pycache__/squat.cpython-39.pyc
ADDED
|
Binary file (2.52 kB). View file
|
|
|
exercises/hammer_curl.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
from pose_estimation.angle_calculation import calculate_angle
|
| 4 |
+
from voice_feedback.feedback import provide_hammer_curl_feedback , speak
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class HammerCurl:
|
| 8 |
+
def __init__(self):
|
| 9 |
+
self.counter_right = 0
|
| 10 |
+
self.counter_left = 0
|
| 11 |
+
self.stage_right = None # 'up' or 'down' for right arm
|
| 12 |
+
self.stage_left = None # 'up' or 'down' for left arm
|
| 13 |
+
|
| 14 |
+
self.angle_threshold = 40 # Angle threshold for misalignment
|
| 15 |
+
self.flexion_angle_up = 155 # Flexion angle for 'up' stage
|
| 16 |
+
self.flexion_angle_down = 35 # Flexion angle for 'down' stage
|
| 17 |
+
|
| 18 |
+
self.angle_threshold_up = 155 # Upper threshold for 'up' stage
|
| 19 |
+
self.angle_threshold_down = 47 # Lower threshold for 'down' stage
|
| 20 |
+
|
| 21 |
+
def calculate_shoulder_elbow_hip_angle(self, shoulder, elbow, hip):
|
| 22 |
+
"""Calculate the angle between shoulder, elbow, and hip."""
|
| 23 |
+
return calculate_angle(elbow, shoulder, hip)
|
| 24 |
+
|
| 25 |
+
def calculate_shoulder_elbow_wrist(self, shoulder, elbow, wrist):
|
| 26 |
+
"""Calculate the angle between shoulder, elbow, and wrist."""
|
| 27 |
+
return calculate_angle(shoulder, elbow, wrist)
|
| 28 |
+
|
| 29 |
+
def track_hammer_curl(self, landmarks, frame):
|
| 30 |
+
# Right arm landmarks (shoulder, elbow, hip, wrist)
|
| 31 |
+
shoulder_right = [int(landmarks[11].x * frame.shape[1]), int(landmarks[11].y * frame.shape[0])]
|
| 32 |
+
elbow_right = [int(landmarks[13].x * frame.shape[1]), int(landmarks[13].y * frame.shape[0])]
|
| 33 |
+
hip_right = [int(landmarks[23].x * frame.shape[1]), int(landmarks[23].y * frame.shape[0])]
|
| 34 |
+
wrist_right = [int(landmarks[15].x * frame.shape[1]), int(landmarks[15].y * frame.shape[0])]
|
| 35 |
+
|
| 36 |
+
# Left arm landmarks (shoulder, elbow, hip, wrist)
|
| 37 |
+
shoulder_left = [int(landmarks[12].x * frame.shape[1]), int(landmarks[12].y * frame.shape[0])]
|
| 38 |
+
elbow_left = [int(landmarks[14].x * frame.shape[1]), int(landmarks[14].y * frame.shape[0])]
|
| 39 |
+
hip_left = [int(landmarks[24].x * frame.shape[1]), int(landmarks[24].y * frame.shape[0])]
|
| 40 |
+
wrist_left = [int(landmarks[16].x * frame.shape[1]), int(landmarks[16].y * frame.shape[0])]
|
| 41 |
+
|
| 42 |
+
# Calculate the angle for counting (elbow flexion angle)
|
| 43 |
+
angle_right_counter = self.calculate_shoulder_elbow_wrist(shoulder_right, elbow_right, wrist_right)
|
| 44 |
+
angle_left_counter = self.calculate_shoulder_elbow_wrist(shoulder_left, elbow_left, wrist_left)
|
| 45 |
+
|
| 46 |
+
# Calculate the angle for the right arm (shoulder, elbow, hip)
|
| 47 |
+
angle_right = self.calculate_shoulder_elbow_hip_angle(shoulder_right, elbow_right, hip_right)
|
| 48 |
+
|
| 49 |
+
# Calculate the angle for the left arm (shoulder, elbow, hip)
|
| 50 |
+
angle_left = self.calculate_shoulder_elbow_hip_angle(shoulder_left, elbow_left, hip_left)
|
| 51 |
+
|
| 52 |
+
# Draw lines with improved style
|
| 53 |
+
self.draw_line_with_style(frame, shoulder_left, elbow_left, (0, 0, 255), 4)
|
| 54 |
+
self.draw_line_with_style(frame, elbow_left, wrist_left, (0, 0, 255), 4)
|
| 55 |
+
|
| 56 |
+
self.draw_line_with_style(frame, shoulder_right, elbow_right, (0, 0, 255), 4)
|
| 57 |
+
self.draw_line_with_style(frame, elbow_right, wrist_right, (0, 0, 255), 4)
|
| 58 |
+
|
| 59 |
+
# Add circles to highlight key points
|
| 60 |
+
self.draw_circle(frame, shoulder_left, (0, 0, 255), 8)
|
| 61 |
+
self.draw_circle(frame, elbow_left, (0, 0, 255), 8)
|
| 62 |
+
self.draw_circle(frame, wrist_left, (0, 0, 255), 8)
|
| 63 |
+
|
| 64 |
+
self.draw_circle(frame, shoulder_right, (0, 0, 255), 8)
|
| 65 |
+
self.draw_circle(frame, elbow_right, (0, 0, 255), 8)
|
| 66 |
+
self.draw_circle(frame, wrist_right, (0, 0, 255), 8)
|
| 67 |
+
|
| 68 |
+
# Convert the angles to integers and update the text positions
|
| 69 |
+
angle_text_position_left = (elbow_left[0] + 10, elbow_left[1] - 10)
|
| 70 |
+
cv2.putText(frame, f'Angle: {int(angle_left_counter)}', angle_text_position_left, cv2.FONT_HERSHEY_SIMPLEX, 0.5,
|
| 71 |
+
(255, 255, 255), 2)
|
| 72 |
+
|
| 73 |
+
angle_text_position_right = (elbow_right[0] + 10, elbow_right[1] - 10)
|
| 74 |
+
cv2.putText(frame, f'Angle: {int(angle_right_counter)}', angle_text_position_right, cv2.FONT_HERSHEY_SIMPLEX,
|
| 75 |
+
0.5,
|
| 76 |
+
(255, 255, 255), 2)
|
| 77 |
+
|
| 78 |
+
warning_message_right = None
|
| 79 |
+
warning_message_left = None
|
| 80 |
+
|
| 81 |
+
# Check for misalignment based on shoulder-elbow-hip angle
|
| 82 |
+
if abs(angle_right) > self.angle_threshold:
|
| 83 |
+
warning_message_right = f"Right Shoulder-Elbow-Hip Misalignment! Angle: {angle_right:.2f}°"
|
| 84 |
+
if abs(angle_left) > self.angle_threshold:
|
| 85 |
+
warning_message_left = f"Left Shoulder-Elbow-Hip Misalignment! Angle: {angle_left:.2f}°"
|
| 86 |
+
|
| 87 |
+
if angle_right_counter > self.angle_threshold_up:
|
| 88 |
+
self.stage_right = "Flex"
|
| 89 |
+
elif self.angle_threshold_down < angle_right_counter < self.angle_threshold_up and self.stage_right == "Flex":
|
| 90 |
+
self.stage_right = "Up"
|
| 91 |
+
elif angle_right_counter < self.angle_threshold_down and self.stage_right=="Up":
|
| 92 |
+
self.stage_right = "Down"
|
| 93 |
+
self.counter_right +=1
|
| 94 |
+
|
| 95 |
+
if angle_left_counter > self.angle_threshold_up:
|
| 96 |
+
self.stage_left = "Flex"
|
| 97 |
+
elif self.angle_threshold_down < angle_left_counter < self.angle_threshold_up and self.stage_left == "Flex":
|
| 98 |
+
self.stage_left = "Up"
|
| 99 |
+
elif angle_left_counter < self.angle_threshold_down and self.stage_left == "Up":
|
| 100 |
+
self.stage_left = "Down"
|
| 101 |
+
self.counter_left +=1
|
| 102 |
+
|
| 103 |
+
# Progress percentages: 1 for "up", 0 for "down"
|
| 104 |
+
progress_right = 1 if self.stage_right == "up" else 0
|
| 105 |
+
progress_left = 1 if self.stage_left == "up" else 0
|
| 106 |
+
|
| 107 |
+
return self.counter_right, angle_right_counter, self.counter_left, angle_left_counter, warning_message_right, warning_message_left, progress_right, progress_left, self.stage_right, self.stage_left
|
| 108 |
+
|
| 109 |
+
def draw_line_with_style(self, frame, start_point, end_point, color, thickness):
|
| 110 |
+
cv2.line(frame, start_point, end_point, color, thickness, lineType=cv2.LINE_AA)
|
| 111 |
+
|
| 112 |
+
def draw_circle(self, frame, center, color, radius):
|
| 113 |
+
"""Draw a circle with specified style."""
|
| 114 |
+
cv2.circle(frame, center, radius, color, -1) # -1 to fill the circle
|
exercises/push_up.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import time
|
| 3 |
+
from pose_estimation.angle_calculation import calculate_angle
|
| 4 |
+
|
| 5 |
+
class PushUp:
|
| 6 |
+
def __init__(self):
|
| 7 |
+
self.counter = 0
|
| 8 |
+
self.stage = "Initial" # 'up' or 'down'
|
| 9 |
+
self.angle_threshold_up = 150 # Upper threshold for 'up' stage
|
| 10 |
+
self.angle_threshold_down = 70 # Lower threshold for 'down' stage
|
| 11 |
+
self.last_counter_update = time.time() # Track the time of the last counter update
|
| 12 |
+
|
| 13 |
+
def calculate_shoulder_elbow_wrist_angle(self, shoulder, elbow, wrist):
|
| 14 |
+
"""Calculate the angle between shoulder, elbow, and wrist."""
|
| 15 |
+
return calculate_angle(shoulder, elbow, wrist)
|
| 16 |
+
|
| 17 |
+
def track_push_up(self, landmarks, frame):
|
| 18 |
+
# Right side landmarks (shoulder, elbow, wrist)
|
| 19 |
+
shoulder_left = [int(landmarks[11].x * frame.shape[1]), int(landmarks[11].y * frame.shape[0])]
|
| 20 |
+
elbow_left = [int(landmarks[13].x * frame.shape[1]), int(landmarks[13].y * frame.shape[0])]
|
| 21 |
+
wrist_left = [int(landmarks[15].x * frame.shape[1]), int(landmarks[15].y * frame.shape[0])]
|
| 22 |
+
|
| 23 |
+
shoulder_right = [int(landmarks[12].x * frame.shape[1]), int(landmarks[12].y * frame.shape[0])]
|
| 24 |
+
elbow_right = [int(landmarks[14].x * frame.shape[1]), int(landmarks[14].y * frame.shape[0])]
|
| 25 |
+
wrist_right = [int(landmarks[16].x * frame.shape[1]), int(landmarks[16].y * frame.shape[0])]
|
| 26 |
+
|
| 27 |
+
# Calculate angles for push-up tracking
|
| 28 |
+
angle_left = self.calculate_shoulder_elbow_wrist_angle(shoulder_left, elbow_left, wrist_left)
|
| 29 |
+
angle_right = self.calculate_shoulder_elbow_wrist_angle(shoulder_right, elbow_right, wrist_right)
|
| 30 |
+
|
| 31 |
+
# Draw lines with improved style
|
| 32 |
+
self.draw_line_with_style(frame, shoulder_left, elbow_left, (0, 0, 255), 2)
|
| 33 |
+
self.draw_line_with_style(frame, elbow_left, wrist_left, (0, 0, 255), 2)
|
| 34 |
+
|
| 35 |
+
self.draw_line_with_style(frame, shoulder_right, elbow_right, (102, 0, 0), 2)
|
| 36 |
+
self.draw_line_with_style(frame, elbow_right, wrist_right, (102, 0, 0), 2)
|
| 37 |
+
|
| 38 |
+
# Draw circles to highlight key points
|
| 39 |
+
self.draw_circle(frame, shoulder_left, (0, 0, 255), 8)
|
| 40 |
+
self.draw_circle(frame, elbow_left, (0, 0, 255), 8)
|
| 41 |
+
self.draw_circle(frame, wrist_left, (0, 0, 255), 8)
|
| 42 |
+
|
| 43 |
+
self.draw_circle(frame, shoulder_right, (102, 0, 0), 8)
|
| 44 |
+
self.draw_circle(frame, elbow_right, (102, 0, 0), 8)
|
| 45 |
+
self.draw_circle(frame, wrist_right, (102, 0, 0), 8)
|
| 46 |
+
|
| 47 |
+
# Update angle text positions and display
|
| 48 |
+
angle_text_position_left = (elbow_left[0] + 10, elbow_left[1] - 10)
|
| 49 |
+
cv2.putText(frame, f'Angle: {int(angle_left)}', angle_text_position_left, cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
|
| 50 |
+
|
| 51 |
+
angle_text_position_right = (elbow_right[0] + 10, elbow_right[1] - 10)
|
| 52 |
+
cv2.putText(frame, f'Angle: {int(angle_right)}', angle_text_position_right, cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
|
| 53 |
+
|
| 54 |
+
# Get current time
|
| 55 |
+
current_time = time.time()
|
| 56 |
+
|
| 57 |
+
# Update stage and counter
|
| 58 |
+
if angle_left > self.angle_threshold_up:
|
| 59 |
+
self.stage = "Starting position"
|
| 60 |
+
elif self.angle_threshold_down < angle_left < self.angle_threshold_up and self.stage == "Starting position":
|
| 61 |
+
self.stage = "Descent"
|
| 62 |
+
elif angle_left < self.angle_threshold_down and self.stage == "Descent":
|
| 63 |
+
self.stage = "Ascent"
|
| 64 |
+
# Increment counter only if enough time has passed since last update
|
| 65 |
+
if current_time - self.last_counter_update > 1: # 1 second threshold
|
| 66 |
+
self.counter += 1
|
| 67 |
+
self.last_counter_update = current_time
|
| 68 |
+
|
| 69 |
+
return self.counter, angle_left, self.stage
|
| 70 |
+
|
| 71 |
+
def draw_line_with_style(self, frame, start_point, end_point, color, thickness):
|
| 72 |
+
"""Draw a line with specified style."""
|
| 73 |
+
cv2.line(frame, start_point, end_point, color, thickness, lineType=cv2.LINE_AA)
|
| 74 |
+
|
| 75 |
+
def draw_circle(self, frame, center, color, radius):
|
| 76 |
+
"""Draw a circle with specified style."""
|
| 77 |
+
cv2.circle(frame, center, radius, color, -1) # -1 to fill the circle
|
exercises/squat.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
from pose_estimation.angle_calculation import calculate_angle
|
| 3 |
+
|
| 4 |
+
class Squat:
|
| 5 |
+
def __init__(self):
|
| 6 |
+
self.counter = 0
|
| 7 |
+
self.stage = None
|
| 8 |
+
|
| 9 |
+
def calculate_angle(self, hip, knee, ankle):
|
| 10 |
+
return calculate_angle(hip, knee, ankle)
|
| 11 |
+
|
| 12 |
+
def track_squat(self, landmarks, frame):
|
| 13 |
+
# Landmark coordinates
|
| 14 |
+
hip = [int(landmarks[23].x * frame.shape[1]), int(landmarks[23].y * frame.shape[0])]
|
| 15 |
+
knee = [int(landmarks[25].x * frame.shape[1]), int(landmarks[25].y * frame.shape[0])]
|
| 16 |
+
shoulder = [int(landmarks[11].x * frame.shape[1]), int(landmarks[11].y * frame.shape[0])]
|
| 17 |
+
|
| 18 |
+
hip_right = [int(landmarks[24].x * frame.shape[1]), int(landmarks[24].y * frame.shape[0])]
|
| 19 |
+
knee_right = [int(landmarks[26].x * frame.shape[1]), int(landmarks[26].y * frame.shape[0])]
|
| 20 |
+
shoulder_right = [int(landmarks[12].x * frame.shape[1]), int(landmarks[12].y * frame.shape[0])]
|
| 21 |
+
|
| 22 |
+
# Calculate angles
|
| 23 |
+
angle = self.calculate_angle(shoulder, hip, knee)
|
| 24 |
+
angle_right = self.calculate_angle(shoulder_right, hip_right, knee_right)
|
| 25 |
+
|
| 26 |
+
# Draw lines and circles to highlight key points
|
| 27 |
+
self.draw_line_with_style(frame, shoulder, hip, (178, 102, 255), 2)
|
| 28 |
+
self.draw_line_with_style(frame, hip, knee, (178, 102, 255), 2)
|
| 29 |
+
self.draw_line_with_style(frame, shoulder_right, hip_right, (51, 153, 255), 2)
|
| 30 |
+
self.draw_line_with_style(frame, hip_right, knee_right, (51, 153, 255), 2)
|
| 31 |
+
|
| 32 |
+
self.draw_circle(frame, shoulder, (178, 102, 255), 8)
|
| 33 |
+
self.draw_circle(frame, hip, (178, 102, 255), 8)
|
| 34 |
+
self.draw_circle(frame, knee, (178, 102, 255), 8)
|
| 35 |
+
self.draw_circle(frame, shoulder_right, (51, 153, 255), 8)
|
| 36 |
+
self.draw_circle(frame, hip_right, (51, 153, 255), 8)
|
| 37 |
+
self.draw_circle(frame, knee_right, (51, 153, 255), 8)
|
| 38 |
+
|
| 39 |
+
# Display angles on screen
|
| 40 |
+
angle_text_position = (knee[0] + 10, knee[1] - 10)
|
| 41 |
+
cv2.putText(frame, f'Angle Left: {int(angle)}', angle_text_position, cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
|
| 42 |
+
|
| 43 |
+
angle_text_position_right = (knee_right[0] + 10, knee_right[1] - 10)
|
| 44 |
+
cv2.putText(frame, f'Angle Right: {int(angle_right)}', angle_text_position_right, cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
|
| 45 |
+
|
| 46 |
+
# Update exercise stage and counter
|
| 47 |
+
if angle > 170:
|
| 48 |
+
self.stage = "Starting Position"
|
| 49 |
+
elif 90 < angle < 170 and self.stage == "Starting Position":
|
| 50 |
+
self.stage = "Descent"
|
| 51 |
+
elif angle < 90 and self.stage == "Descent":
|
| 52 |
+
self.stage = "Ascent"
|
| 53 |
+
self.counter += 1
|
| 54 |
+
return self.counter, angle, self.stage
|
| 55 |
+
|
| 56 |
+
def draw_line_with_style(self, frame, start_point, end_point, color, thickness):
|
| 57 |
+
"""Draw a line with specified style."""
|
| 58 |
+
cv2.line(frame, start_point, end_point, color, thickness, lineType=cv2.LINE_AA)
|
| 59 |
+
|
| 60 |
+
def draw_circle(self, frame, center, color, radius):
|
| 61 |
+
"""Draw a circle with specified style."""
|
| 62 |
+
cv2.circle(frame, center, radius, color, -1) # -1 to fill the circle
|
feedback/__pycache__/indicators.cpython-311.pyc
ADDED
|
Binary file (2.96 kB). View file
|
|
|
feedback/__pycache__/indicators.cpython-312.pyc
ADDED
|
Binary file (2.52 kB). View file
|
|
|
feedback/__pycache__/indicators.cpython-39.pyc
ADDED
|
Binary file (1.93 kB). View file
|
|
|
feedback/__pycache__/information.cpython-311.pyc
ADDED
|
Binary file (1.14 kB). View file
|
|
|
feedback/__pycache__/information.cpython-312.pyc
ADDED
|
Binary file (1.04 kB). View file
|
|
|
feedback/__pycache__/information.cpython-39.pyc
ADDED
|
Binary file (951 Bytes). View file
|
|
|
feedback/__pycache__/layout.cpython-311.pyc
ADDED
|
Binary file (1.17 kB). View file
|
|
|
feedback/__pycache__/layout.cpython-312.pyc
ADDED
|
Binary file (992 Bytes). View file
|
|
|
feedback/__pycache__/layout.cpython-39.pyc
ADDED
|
Binary file (814 Bytes). View file
|
|
|
feedback/indicators.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# feedback/indicators.py
|
| 2 |
+
from utils.drawing_utils import draw_gauge_meter,draw_progress_bar,display_stage,display_counter
|
| 3 |
+
|
| 4 |
+
display_counter_poisiton=(40, 240)
|
| 5 |
+
display_stage_poisiton=(40, 270)
|
| 6 |
+
display_counter_angel_color=(255,255,0)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def draw_squat_indicators(frame, counter, angle, stage):
|
| 10 |
+
# Counter
|
| 11 |
+
display_counter(frame,counter, position=display_counter_poisiton, color=(0, 0, 0),background_color=(192,192,192))
|
| 12 |
+
|
| 13 |
+
# Stage
|
| 14 |
+
display_stage(frame, stage,"Stage", position=display_stage_poisiton, color=(0, 0, 0),background_color=(192,192,192))
|
| 15 |
+
|
| 16 |
+
draw_progress_bar(frame, exercise="squat", value=counter, position=(40, 170), size=(200, 20), color=(163, 245, 184, 1),background_color=(255,255,255))
|
| 17 |
+
|
| 18 |
+
draw_gauge_meter(frame, angle=angle, text="Squat Gauge Meter", position=(135, 415), radius=75, color=(0, 0, 255))
|
| 19 |
+
|
| 20 |
+
def draw_pushup_indicators(frame, counter, angle, stage):
|
| 21 |
+
# Counter
|
| 22 |
+
display_counter(frame,counter, position=display_counter_poisiton, color=(0, 0, 0),background_color=(192,192,192))
|
| 23 |
+
|
| 24 |
+
display_stage(frame, stage,"Stage", position=display_stage_poisiton, color=(0, 0, 0),background_color=(192,192,192))
|
| 25 |
+
draw_progress_bar(frame, exercise="push_up", value=counter, position=(40, 170), size=(200, 20), color=(163, 245, 184, 1),background_color=(255,255,255))
|
| 26 |
+
|
| 27 |
+
text = "Push-u Gauge Meter"
|
| 28 |
+
draw_gauge_meter(frame, angle=angle,text=text, position=(350,80), radius=50, color=(0, 102, 204))
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def draw_hammercurl_indicators(frame, counter_right, angle_right, counter_left, angle_left, stage_right, stage_left):
|
| 32 |
+
display_counter_poisiton_left_arm = (40, 300)
|
| 33 |
+
|
| 34 |
+
# Right Arm Indicators
|
| 35 |
+
display_counter(frame, counter_right, position=display_counter_poisiton, color=(0, 0, 0),background_color=(192,192,192))
|
| 36 |
+
|
| 37 |
+
display_stage(frame, stage_right,"Right Stage", position=display_stage_poisiton, color=(0, 0, 0),background_color=(192,192,192))
|
| 38 |
+
display_stage(frame, stage_left,"Left Stage", position=display_counter_poisiton_left_arm, color=(0, 0, 0),background_color=(192,192,192))
|
| 39 |
+
|
| 40 |
+
# Progress Bars
|
| 41 |
+
draw_progress_bar(frame, exercise="hammer_curl", value=(counter_right+counter_left)/2, position=(40, 170), size=(200, 20), color=(163, 245, 184, 1),background_color=(255,255,255))
|
| 42 |
+
|
| 43 |
+
text_right = "Right Gauge Meter"
|
| 44 |
+
text_left = "Left Gauge Meter"
|
| 45 |
+
|
| 46 |
+
# Gauge Meters for Angles
|
| 47 |
+
draw_gauge_meter(frame, angle=angle_right,text=text_right, position=(1200,80), radius=50, color=(0, 102, 204))
|
| 48 |
+
draw_gauge_meter(frame, angle=angle_left,text=text_left, position=(1200,240), radius=50, color=(0, 102, 204))
|
| 49 |
+
|
| 50 |
+
|
feedback/information.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def get_exercise_info(exercise_type):
|
| 2 |
+
exercises = {
|
| 3 |
+
"hammer_curl": {
|
| 4 |
+
"name": "Hammer Curl",
|
| 5 |
+
"target_muscles": ["Biceps", "Brachialis"],
|
| 6 |
+
"equipment": "Dumbbells",
|
| 7 |
+
"reps": 8,
|
| 8 |
+
"sets": 1,
|
| 9 |
+
"rest_time": "60 seconds",
|
| 10 |
+
"benefits": [
|
| 11 |
+
"Improves bicep and forearm strength",
|
| 12 |
+
"Enhances grip strength"
|
| 13 |
+
]
|
| 14 |
+
},
|
| 15 |
+
"push_up": {
|
| 16 |
+
"name": "Push-Up",
|
| 17 |
+
"target_muscles": ["Chest", "Triceps", "Shoulders"],
|
| 18 |
+
"equipment": "Bodyweight",
|
| 19 |
+
"reps": 10,
|
| 20 |
+
"sets": 1,
|
| 21 |
+
"rest_time": "45 seconds",
|
| 22 |
+
"benefits": [
|
| 23 |
+
"Builds upper body strength",
|
| 24 |
+
"Improves core stability"
|
| 25 |
+
]
|
| 26 |
+
},
|
| 27 |
+
"squat": {
|
| 28 |
+
"name": "Squat",
|
| 29 |
+
"target_muscles": ["Quads", "Glutes", "Hamstrings"],
|
| 30 |
+
"equipment": "Bodyweight or Barbell",
|
| 31 |
+
"reps": 2,
|
| 32 |
+
"sets": 3,
|
| 33 |
+
"rest_time": "60 seconds",
|
| 34 |
+
"benefits": [
|
| 35 |
+
"Builds lower body strength",
|
| 36 |
+
"Improves mobility and balance"
|
| 37 |
+
]
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
return exercises.get(exercise_type, {})
|
feedback/layout.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# feedback/layout.py
|
| 2 |
+
|
| 3 |
+
from feedback.indicators import draw_squat_indicators, draw_pushup_indicators, draw_hammercurl_indicators
|
| 4 |
+
|
| 5 |
+
def layout_indicators(frame, exercise_type, exercise_data):
|
| 6 |
+
if exercise_type == "squat":
|
| 7 |
+
counter, angle, stage = exercise_data
|
| 8 |
+
draw_squat_indicators(frame, counter, angle, stage)
|
| 9 |
+
elif exercise_type == "push_up":
|
| 10 |
+
counter, angle, stage = exercise_data
|
| 11 |
+
draw_pushup_indicators(frame, counter, angle, stage)
|
| 12 |
+
elif exercise_type == "hammer_curl":
|
| 13 |
+
(counter_right, angle_right, counter_left, angle_left,
|
| 14 |
+
warning_message_right, warning_message_left, progress_right, progress_left,stage_right,stage_left) = exercise_data
|
| 15 |
+
draw_hammercurl_indicators(frame, counter_right, angle_right, counter_left, angle_left, stage_right,stage_left)
|
| 16 |
+
|
main.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
from pose_estimation.estimation import PoseEstimator
|
| 3 |
+
from exercises.squat import Squat
|
| 4 |
+
from exercises.hammer_curl import HammerCurl
|
| 5 |
+
from exercises.push_up import PushUp
|
| 6 |
+
from feedback.layout import layout_indicators
|
| 7 |
+
from feedback.information import get_exercise_info
|
| 8 |
+
from utils.draw_text_with_background import draw_text_with_background
|
| 9 |
+
|
| 10 |
+
def main():
|
| 11 |
+
video_path = r"C:\Users\yakupzengin\Fitness-Trainer\data\squat.mp4"
|
| 12 |
+
video_path = r"C:\Users\yakupzengin\Fitness-Trainer\data\push_up.mp4"
|
| 13 |
+
video_path = r"C:\Users\yakupzengin\Fitness-Trainer\data\dumbel-workout.mp4"
|
| 14 |
+
|
| 15 |
+
exercise_type = "hammer_curl" # Egzersiz türünü belirleyin ("hammer_curl", "squat", "push_up")
|
| 16 |
+
|
| 17 |
+
cap = cv2.VideoCapture(0)
|
| 18 |
+
pose_estimator = PoseEstimator()
|
| 19 |
+
|
| 20 |
+
if exercise_type == "hammer_curl":
|
| 21 |
+
exercise = HammerCurl()
|
| 22 |
+
elif exercise_type == "squat":
|
| 23 |
+
exercise = Squat()
|
| 24 |
+
elif exercise_type == "push_up":
|
| 25 |
+
exercise = PushUp()
|
| 26 |
+
else:
|
| 27 |
+
print("Invalid exercise type.")
|
| 28 |
+
return
|
| 29 |
+
|
| 30 |
+
exercise_info = get_exercise_info(exercise_type)
|
| 31 |
+
|
| 32 |
+
fourcc = cv2.VideoWriter_fourcc(*'XVID')
|
| 33 |
+
output_file = r"C:\Users\yakupzengin\Fitness-Trainer\output\push-up.avi"
|
| 34 |
+
fps = cap.get(cv2.CAP_PROP_FPS)
|
| 35 |
+
frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 36 |
+
frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 37 |
+
out = cv2.VideoWriter(output_file, fourcc, fps, (frame_width, frame_height))
|
| 38 |
+
|
| 39 |
+
while cap.isOpened():
|
| 40 |
+
ret, frame = cap.read()
|
| 41 |
+
if not ret:
|
| 42 |
+
break
|
| 43 |
+
|
| 44 |
+
results = pose_estimator.estimate_pose(frame, exercise_type)
|
| 45 |
+
if results.pose_landmarks:
|
| 46 |
+
if exercise_type == "squat":
|
| 47 |
+
counter, angle, stage = exercise.track_squat(results.pose_landmarks.landmark, frame)
|
| 48 |
+
layout_indicators(frame, exercise_type, (counter, angle, stage))
|
| 49 |
+
elif exercise_type == "hammer_curl":
|
| 50 |
+
(counter_right, angle_right, counter_left, angle_left,
|
| 51 |
+
warning_message_right, warning_message_left, progress_right, progress_left, stage_right, stage_left) = exercise.track_hammer_curl(
|
| 52 |
+
results.pose_landmarks.landmark, frame)
|
| 53 |
+
layout_indicators(frame, exercise_type,
|
| 54 |
+
(counter_right, angle_right, counter_left, angle_left,
|
| 55 |
+
warning_message_right, warning_message_left, progress_right, progress_left, stage_right, stage_left))
|
| 56 |
+
elif exercise_type == "push_up":
|
| 57 |
+
counter, angle, stage = exercise.track_push_up(results.pose_landmarks.landmark, frame)
|
| 58 |
+
layout_indicators(frame, exercise_type, (counter, angle, stage))
|
| 59 |
+
|
| 60 |
+
draw_text_with_background(frame, f"Exercise: {exercise_info.get('name', 'N/A')}", (40, 50),
|
| 61 |
+
cv2.FONT_HERSHEY_DUPLEX, 0.7, (255, 255, 255,), (118, 29, 14, 0.79), 1)
|
| 62 |
+
draw_text_with_background(frame, f"Reps: {exercise_info.get('reps', 0)}", (40, 80),
|
| 63 |
+
cv2.FONT_HERSHEY_DUPLEX, 0.7, (255, 255, 255,), (118, 29, 14, 0.79), 1)
|
| 64 |
+
draw_text_with_background(frame, f"Sets: {exercise_info.get('sets', 0)}", (40, 110),
|
| 65 |
+
cv2.FONT_HERSHEY_DUPLEX, 0.7, (255, 255, 255,), (118, 29, 14, 0.79),1 )
|
| 66 |
+
|
| 67 |
+
out.write(frame)
|
| 68 |
+
|
| 69 |
+
cv2.namedWindow(f"{exercise_type.replace('_', ' ').title()} Tracker", cv2.WINDOW_NORMAL)
|
| 70 |
+
cv2.resizeWindow(f"{exercise_type.replace('_', ' ').title()} Tracker", 1920, 1080)
|
| 71 |
+
cv2.imshow(f"{exercise_type.replace('_', ' ').title()} Tracker", frame)
|
| 72 |
+
|
| 73 |
+
if cv2.waitKey(10) & 0xFF == ord('q'):
|
| 74 |
+
break
|
| 75 |
+
|
| 76 |
+
cap.release()
|
| 77 |
+
out.release()
|
| 78 |
+
cv2.destroyAllWindows()
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
if __name__ == '__main__':
|
| 82 |
+
main()
|
output/images/Screenshot 2024-09-08 030742.png
ADDED
|
Git LFS Details
|
output/images/Screenshot 2024-09-08 030816.png
ADDED
|
Git LFS Details
|
output/images/Screenshot 2024-09-08 030836.png
ADDED
|
Git LFS Details
|
pose_estimation/__pycache__/angle_calculation.cpython-311.pyc
ADDED
|
Binary file (1.2 kB). View file
|
|
|
pose_estimation/__pycache__/angle_calculation.cpython-312.pyc
ADDED
|
Binary file (1.04 kB). View file
|
|
|
pose_estimation/__pycache__/angle_calculation.cpython-39.pyc
ADDED
|
Binary file (642 Bytes). View file
|
|
|
pose_estimation/__pycache__/estimation.cpython-311.pyc
ADDED
|
Binary file (8.2 kB). View file
|
|
|
pose_estimation/__pycache__/estimation.cpython-312.pyc
ADDED
|
Binary file (8.09 kB). View file
|
|
|
pose_estimation/__pycache__/estimation.cpython-39.pyc
ADDED
|
Binary file (3.23 kB). View file
|
|
|
pose_estimation/angle_calculation.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
|
| 3 |
+
def calculate_angle(a,b,c):
|
| 4 |
+
# abc npktaları [x,y,z] noktaları
|
| 5 |
+
|
| 6 |
+
ba = [a[0] - b[0], a[1] - b[1]]
|
| 7 |
+
bc = [c[0] - b[0], c[1] - b[1]]
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# dot product (iç çarpım) hesapla: ba · bc
|
| 11 |
+
dot_product = ba[0] * bc[0] + ba[1] * bc[1]
|
| 12 |
+
|
| 13 |
+
# Vektörlerin büyüklüklerini hesapla
|
| 14 |
+
magnitude_ba = math.sqrt(ba[0] ** 2 + ba[1] ** 2)
|
| 15 |
+
magnitude_bc = math.sqrt(bc[0] ** 2 + bc[1] ** 2)
|
| 16 |
+
|
| 17 |
+
# Kosinüs açısını hesapla
|
| 18 |
+
cosine_angle = dot_product / (magnitude_ba * magnitude_bc)
|
| 19 |
+
|
| 20 |
+
# Açı hesapla ve dereceye çevir
|
| 21 |
+
angle = math.degrees(math.acos(cosine_angle))
|
| 22 |
+
|
| 23 |
+
return angle
|
pose_estimation/estimation.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import mediapipe as mp
|
| 3 |
+
from exercises.hammer_curl import HammerCurl
|
| 4 |
+
|
| 5 |
+
class PoseEstimator:
|
| 6 |
+
def __init__(self):
|
| 7 |
+
self.mp_pose = mp.solutions.pose
|
| 8 |
+
self.pose = self.mp_pose.Pose()
|
| 9 |
+
self.mp_drawing = mp.solutions.drawing_utils
|
| 10 |
+
|
| 11 |
+
def estimate_pose(self, frame, exercise_type):
|
| 12 |
+
# BGR to RGB
|
| 13 |
+
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 14 |
+
|
| 15 |
+
# Pose estimate
|
| 16 |
+
results = self.pose.process(rgb_frame)
|
| 17 |
+
|
| 18 |
+
# Draw landmarks and specific connections based on exercise type
|
| 19 |
+
if results.pose_landmarks:
|
| 20 |
+
# Draw specific landmarks and connections based on exercise_type
|
| 21 |
+
if exercise_type == "squat":
|
| 22 |
+
self.draw_squat_lines(frame, results.pose_landmarks.landmark)
|
| 23 |
+
elif exercise_type == "push_up":
|
| 24 |
+
self.draw_push_up_lines(frame, results.pose_landmarks.landmark)
|
| 25 |
+
elif exercise_type == "hammer_curl":
|
| 26 |
+
self.draw_hammerl_curl_lines(frame, results.pose_landmarks.landmark)
|
| 27 |
+
|
| 28 |
+
return results
|
| 29 |
+
def draw_hammerl_curl_lines(self, frame, landmarks):
|
| 30 |
+
|
| 31 |
+
shoulder_right = [int(landmarks[11].x * frame.shape[1]), int(landmarks[11].y * frame.shape[0])]
|
| 32 |
+
elbow_right = [int(landmarks[13].x * frame.shape[1]), int(landmarks[13].y * frame.shape[0])]
|
| 33 |
+
hip_right = [int(landmarks[23].x * frame.shape[1]), int(landmarks[23].y * frame.shape[0])]
|
| 34 |
+
wrist_right = [int(landmarks[15].x * frame.shape[1]), int(landmarks[15].y * frame.shape[0])]
|
| 35 |
+
|
| 36 |
+
# Left arm landmarks (shoulder, elbow, hip, wrist)
|
| 37 |
+
shoulder_left = [int(landmarks[12].x * frame.shape[1]), int(landmarks[12].y * frame.shape[0])]
|
| 38 |
+
elbow_left = [int(landmarks[14].x * frame.shape[1]), int(landmarks[14].y * frame.shape[0])]
|
| 39 |
+
hip_left = [int(landmarks[24].x * frame.shape[1]), int(landmarks[24].y * frame.shape[0])]
|
| 40 |
+
wrist_left = [int(landmarks[16].x * frame.shape[1]), int(landmarks[16].y * frame.shape[0])]
|
| 41 |
+
|
| 42 |
+
# Draw lines with improved style
|
| 43 |
+
cv2.line(frame, shoulder_left, elbow_left, (0, 0, 255), 4,2)
|
| 44 |
+
cv2.line(frame, elbow_left, wrist_left, (0, 0, 255), 4,2)
|
| 45 |
+
|
| 46 |
+
cv2.line(frame, shoulder_right, elbow_right, (0, 0, 255), 4,2)
|
| 47 |
+
cv2.line(frame, elbow_right, wrist_right, (0, 0, 255), 4,2)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def draw_squat_lines(self, frame, landmarks):
|
| 52 |
+
# Squat specific lines (hip, knee, shoulder)
|
| 53 |
+
hip = [int(landmarks[23].x * frame.shape[1]), int(landmarks[23].y * frame.shape[0])]
|
| 54 |
+
knee = [int(landmarks[25].x * frame.shape[1]), int(landmarks[25].y * frame.shape[0])]
|
| 55 |
+
shoulder = [int(landmarks[11].x * frame.shape[1]), int(landmarks[11].y * frame.shape[0])]
|
| 56 |
+
|
| 57 |
+
hip_right = [int(landmarks[24].x * frame.shape[1]), int(landmarks[24].y * frame.shape[0])]
|
| 58 |
+
knee_right = [int(landmarks[26].x * frame.shape[1]), int(landmarks[26].y * frame.shape[0])]
|
| 59 |
+
shoulder_right = [int(landmarks[12].x * frame.shape[1]), int(landmarks[12].y * frame.shape[0])]
|
| 60 |
+
|
| 61 |
+
# Draw lines for squat
|
| 62 |
+
cv2.line(frame, shoulder, hip, (178, 102, 255), 2)
|
| 63 |
+
cv2.line(frame, hip, knee, (178, 102, 255), 2)
|
| 64 |
+
cv2.line(frame, shoulder_right, hip_right, (51, 153, 255), 2)
|
| 65 |
+
cv2.line(frame, hip_right, knee_right, (51, 153, 255), 2)
|
| 66 |
+
|
| 67 |
+
def draw_push_up_lines(self, frame, landmarks):
|
| 68 |
+
# Push-up specific lines (shoulder, elbow, wrist)
|
| 69 |
+
shoulder_left = [int(landmarks[11].x * frame.shape[1]), int(landmarks[11].y * frame.shape[0])]
|
| 70 |
+
elbow_left = [int(landmarks[13].x * frame.shape[1]), int(landmarks[13].y * frame.shape[0])]
|
| 71 |
+
wrist_left = [int(landmarks[15].x * frame.shape[1]), int(landmarks[15].y * frame.shape[0])]
|
| 72 |
+
|
| 73 |
+
shoulder_right = [int(landmarks[12].x * frame.shape[1]), int(landmarks[12].y * frame.shape[0])]
|
| 74 |
+
elbow_right = [int(landmarks[14].x * frame.shape[1]), int(landmarks[14].y * frame.shape[0])]
|
| 75 |
+
wrist_right = [int(landmarks[16].x * frame.shape[1]), int(landmarks[16].y * frame.shape[0])]
|
| 76 |
+
|
| 77 |
+
# Draw lines for push-up
|
| 78 |
+
cv2.line(frame, shoulder_left, elbow_left, (0, 0, 255), 2)
|
| 79 |
+
cv2.line(frame, elbow_left, wrist_left, (0, 0, 255), 2)
|
| 80 |
+
cv2.line(frame, shoulder_right, elbow_right, (102, 0, 0), 2)
|
| 81 |
+
cv2.line(frame, elbow_right, wrist_right, (102, 0, 0), 2)
|
requirements.txt
ADDED
|
Binary file (1.4 kB). View file
|
|
|
static/css/dashboard.css
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.main-nav {
|
| 2 |
+
margin-top: 15px;
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
.main-nav ul {
|
| 6 |
+
display: flex;
|
| 7 |
+
list-style: none;
|
| 8 |
+
justify-content: center;
|
| 9 |
+
gap: 25px;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
.nav-link {
|
| 13 |
+
text-decoration: none;
|
| 14 |
+
color: #555;
|
| 15 |
+
font-weight: 500;
|
| 16 |
+
padding: 8px 15px;
|
| 17 |
+
border-radius: 5px;
|
| 18 |
+
transition: all 0.3s;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.nav-link:hover {
|
| 22 |
+
color: #3498db;
|
| 23 |
+
background-color: rgba(52, 152, 219, 0.1);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.nav-link.active {
|
| 27 |
+
color: #3498db;
|
| 28 |
+
border-bottom: 2px solid #3498db;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.dashboard-content {
|
| 32 |
+
display: flex;
|
| 33 |
+
flex-direction: column;
|
| 34 |
+
gap: 30px;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.stats-summary {
|
| 38 |
+
display: flex;
|
| 39 |
+
flex-wrap: wrap;
|
| 40 |
+
gap: 20px;
|
| 41 |
+
justify-content: space-between;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.stat-card {
|
| 45 |
+
flex: 1 1 200px;
|
| 46 |
+
background-color: white;
|
| 47 |
+
border-radius: 10px;
|
| 48 |
+
padding: 20px;
|
| 49 |
+
text-align: center;
|
| 50 |
+
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
|
| 51 |
+
transition: transform 0.3s;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.stat-card:hover {
|
| 55 |
+
transform: translateY(-5px);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.stat-card h3 {
|
| 59 |
+
font-size: 1rem;
|
| 60 |
+
color: #777;
|
| 61 |
+
margin-bottom: 10px;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.stat-value {
|
| 65 |
+
font-size: 2.5rem;
|
| 66 |
+
font-weight: 700;
|
| 67 |
+
color: #3498db;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.charts-container {
|
| 71 |
+
display: flex;
|
| 72 |
+
flex-wrap: wrap;
|
| 73 |
+
gap: 20px;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.chart-card {
|
| 77 |
+
flex: 1 1 400px;
|
| 78 |
+
background-color: white;
|
| 79 |
+
border-radius: 10px;
|
| 80 |
+
padding: 20px;
|
| 81 |
+
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.chart-card h3 {
|
| 85 |
+
font-size: 1.2rem;
|
| 86 |
+
color: #555;
|
| 87 |
+
margin-bottom: 15px;
|
| 88 |
+
text-align: center;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.recent-workouts {
|
| 92 |
+
background-color: white;
|
| 93 |
+
border-radius: 10px;
|
| 94 |
+
padding: 20px;
|
| 95 |
+
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.recent-workouts h2 {
|
| 99 |
+
font-size: 1.5rem;
|
| 100 |
+
color: #3a3a3a;
|
| 101 |
+
margin-bottom: 15px;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.workout-table {
|
| 105 |
+
width: 100%;
|
| 106 |
+
border-collapse: collapse;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.workout-table th,
|
| 110 |
+
.workout-table td {
|
| 111 |
+
padding: 12px 15px;
|
| 112 |
+
text-align: left;
|
| 113 |
+
border-bottom: 1px solid #eee;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.workout-table th {
|
| 117 |
+
background-color: #f9f9f9;
|
| 118 |
+
color: #555;
|
| 119 |
+
font-weight: 500;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.workout-table tr:hover {
|
| 123 |
+
background-color: #f5f5f5;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
@media (max-width: 768px) {
|
| 127 |
+
.stats-summary {
|
| 128 |
+
flex-direction: column;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.charts-container {
|
| 132 |
+
flex-direction: column;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.workout-table {
|
| 136 |
+
font-size: 0.9rem;
|
| 137 |
+
}
|
| 138 |
+
}
|
static/css/style.css
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
* {
|
| 2 |
+
margin: 0;
|
| 3 |
+
padding: 0;
|
| 4 |
+
box-sizing: border-box;
|
| 5 |
+
font-family: 'Roboto', sans-serif;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
body {
|
| 9 |
+
background-color: #f5f5f5;
|
| 10 |
+
color: #333;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
.container {
|
| 14 |
+
max-width: 1200px;
|
| 15 |
+
margin: 0 auto;
|
| 16 |
+
padding: 20px;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
header {
|
| 20 |
+
text-align: center;
|
| 21 |
+
margin-bottom: 30px;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
header h1 {
|
| 25 |
+
font-size: 2.5rem;
|
| 26 |
+
color: #3a3a3a;
|
| 27 |
+
margin-bottom: 10px;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
header p {
|
| 31 |
+
font-size: 1.2rem;
|
| 32 |
+
color: #666;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.main-content {
|
| 36 |
+
display: flex;
|
| 37 |
+
flex-wrap: wrap;
|
| 38 |
+
gap: 20px;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.video-container {
|
| 42 |
+
flex: 1 1 600px;
|
| 43 |
+
border-radius: 10px;
|
| 44 |
+
overflow: hidden;
|
| 45 |
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.video-container img {
|
| 49 |
+
width: 100%;
|
| 50 |
+
height: auto;
|
| 51 |
+
display: block;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.controls {
|
| 55 |
+
flex: 1 1 300px;
|
| 56 |
+
display: flex;
|
| 57 |
+
flex-direction: column;
|
| 58 |
+
gap: 20px;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.controls h2 {
|
| 62 |
+
font-size: 1.5rem;
|
| 63 |
+
margin-bottom: 15px;
|
| 64 |
+
color: #3a3a3a;
|
| 65 |
+
border-bottom: 2px solid #3498db;
|
| 66 |
+
padding-bottom: 5px;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.exercise-options {
|
| 70 |
+
display: flex;
|
| 71 |
+
gap: 10px;
|
| 72 |
+
flex-wrap: wrap;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.exercise-option {
|
| 76 |
+
flex: 1 1 calc(33% - 10px);
|
| 77 |
+
min-width: 80px;
|
| 78 |
+
background-color: white;
|
| 79 |
+
border-radius: 8px;
|
| 80 |
+
overflow: hidden;
|
| 81 |
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
| 82 |
+
transition:
|
| 83 |
+
transform 0.3s,
|
| 84 |
+
box-shadow 0.3s;
|
| 85 |
+
cursor: pointer;
|
| 86 |
+
text-align: center;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.exercise-option:hover {
|
| 90 |
+
transform: translateY(-5px);
|
| 91 |
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.exercise-option.selected {
|
| 95 |
+
border: 2px solid #3498db;
|
| 96 |
+
transform: translateY(-5px);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.exercise-option img {
|
| 100 |
+
width: 100%;
|
| 101 |
+
height: 100px;
|
| 102 |
+
object-fit: cover;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.exercise-option h3 {
|
| 106 |
+
padding: 10px;
|
| 107 |
+
font-size: 1rem;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.config-inputs {
|
| 111 |
+
display: flex;
|
| 112 |
+
gap: 15px;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.input-group {
|
| 116 |
+
flex: 1;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.input-group label {
|
| 120 |
+
display: block;
|
| 121 |
+
margin-bottom: 5px;
|
| 122 |
+
font-weight: 500;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.input-group input {
|
| 126 |
+
width: 100%;
|
| 127 |
+
padding: 8px 12px;
|
| 128 |
+
border: 1px solid #ddd;
|
| 129 |
+
border-radius: 5px;
|
| 130 |
+
font-size: 1rem;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.action-buttons {
|
| 134 |
+
display: flex;
|
| 135 |
+
gap: 10px;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.btn {
|
| 139 |
+
flex: 1;
|
| 140 |
+
padding: 12px 20px;
|
| 141 |
+
border: none;
|
| 142 |
+
border-radius: 5px;
|
| 143 |
+
font-size: 1rem;
|
| 144 |
+
font-weight: 500;
|
| 145 |
+
cursor: pointer;
|
| 146 |
+
transition: background-color 0.3s;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.primary-btn {
|
| 150 |
+
background-color: #3498db;
|
| 151 |
+
color: white;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.primary-btn:hover {
|
| 155 |
+
background-color: #2980b9;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.secondary-btn {
|
| 159 |
+
background-color: #e74c3c;
|
| 160 |
+
color: white;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.secondary-btn:hover {
|
| 164 |
+
background-color: #c0392b;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.btn:disabled {
|
| 168 |
+
background-color: #ccc;
|
| 169 |
+
cursor: not-allowed;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.workout-status {
|
| 173 |
+
background-color: white;
|
| 174 |
+
border-radius: 8px;
|
| 175 |
+
padding: 15px;
|
| 176 |
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.status-display {
|
| 180 |
+
display: flex;
|
| 181 |
+
flex-direction: column;
|
| 182 |
+
gap: 10px;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.status-item {
|
| 186 |
+
display: flex;
|
| 187 |
+
justify-content: space-between;
|
| 188 |
+
padding: 5px 0;
|
| 189 |
+
border-bottom: 1px solid #eee;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.status-label {
|
| 193 |
+
font-weight: 500;
|
| 194 |
+
color: #555;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.status-value {
|
| 198 |
+
font-weight: 700;
|
| 199 |
+
color: #3498db;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
@media (max-width: 768px) {
|
| 203 |
+
.main-content {
|
| 204 |
+
flex-direction: column;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.config-inputs {
|
| 208 |
+
flex-direction: column;
|
| 209 |
+
}
|
| 210 |
+
}
|
static/images/README.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
This directory should contain the following images:
|
| 2 |
+
- squat.png
|
| 3 |
+
- push_up.png
|
| 4 |
+
- hammer_curl.png
|
| 5 |
+
|
| 6 |
+
These images will be used as icons on the exercise selection page.
|
| 7 |
+
You can use any appropriate images for these exercises.
|
static/images/hammer_curl.png
ADDED
|