pjxcharya commited on
Commit
40518b9
·
verified ·
1 Parent(s): d3174bc

initial commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +3 -0
  2. Dockerfile +42 -0
  3. LICENSE +21 -0
  4. README.md +249 -4
  5. app.py +632 -0
  6. create_static_folders.py +63 -0
  7. db/__init__.py +0 -0
  8. db/__pycache__/workout_logger.cpython-311.pyc +0 -0
  9. db/__pycache__/workout_logger.cpython-312.pyc +0 -0
  10. db/__pycache__/workout_logger.cpython-39.pyc +0 -0
  11. db/workout_logger.py +0 -0
  12. exercises/__init__.py +0 -0
  13. exercises/__pycache__/hammer_curl.cpython-311.pyc +0 -0
  14. exercises/__pycache__/hammer_curl.cpython-312.pyc +0 -0
  15. exercises/__pycache__/hammer_curl.cpython-39.pyc +0 -0
  16. exercises/__pycache__/push_up.cpython-311.pyc +0 -0
  17. exercises/__pycache__/push_up.cpython-312.pyc +0 -0
  18. exercises/__pycache__/push_up.cpython-39.pyc +0 -0
  19. exercises/__pycache__/squat.cpython-311.pyc +0 -0
  20. exercises/__pycache__/squat.cpython-312.pyc +0 -0
  21. exercises/__pycache__/squat.cpython-39.pyc +0 -0
  22. exercises/hammer_curl.py +114 -0
  23. exercises/push_up.py +130 -0
  24. exercises/squat.py +142 -0
  25. feedback/__init__.py +0 -0
  26. feedback/__pycache__/indicators.cpython-311.pyc +0 -0
  27. feedback/__pycache__/indicators.cpython-312.pyc +0 -0
  28. feedback/__pycache__/indicators.cpython-39.pyc +0 -0
  29. feedback/__pycache__/information.cpython-311.pyc +0 -0
  30. feedback/__pycache__/information.cpython-312.pyc +0 -0
  31. feedback/__pycache__/information.cpython-39.pyc +0 -0
  32. feedback/__pycache__/layout.cpython-311.pyc +0 -0
  33. feedback/__pycache__/layout.cpython-312.pyc +0 -0
  34. feedback/__pycache__/layout.cpython-39.pyc +0 -0
  35. feedback/indicators.py +50 -0
  36. feedback/information.py +41 -0
  37. feedback/layout.py +16 -0
  38. live_test.html +72 -0
  39. main.py +82 -0
  40. output/.DS_Store +0 -0
  41. output/images/Screenshot 2024-09-08 030742.png +3 -0
  42. output/images/Screenshot 2024-09-08 030816.png +3 -0
  43. output/images/Screenshot 2024-09-08 030836.png +3 -0
  44. pose_estimation/__init__.py +0 -0
  45. pose_estimation/__pycache__/angle_calculation.cpython-311.pyc +0 -0
  46. pose_estimation/__pycache__/angle_calculation.cpython-312.pyc +0 -0
  47. pose_estimation/__pycache__/angle_calculation.cpython-39.pyc +0 -0
  48. pose_estimation/__pycache__/estimation.cpython-311.pyc +0 -0
  49. pose_estimation/__pycache__/estimation.cpython-312.pyc +0 -0
  50. pose_estimation/__pycache__/estimation.cpython-39.pyc +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
Dockerfile ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.10-slim
3
+
4
+ # Set the working directory in the container
5
+ WORKDIR /app
6
+
7
+ # Create static folder structure (if needed by original UI, less critical for pure API)
8
+ RUN mkdir -p static/images
9
+
10
+ # Install system dependencies required by OpenCV and other libraries
11
+ RUN apt-get update && \
12
+ apt-get install -y --no-install-recommends \
13
+ libgl1-mesa-glx \
14
+ libglib2.0-0 \
15
+ && rm -rf /var/lib/apt/lists/*
16
+
17
+ # Copy the requirements file into the container at /app
18
+ COPY requirements.txt .
19
+
20
+ # Install any needed packages specified in requirements.txt
21
+ # Ensure your requirements.txt is clean and includes gunicorn
22
+ RUN pip install --no-cache-dir -r requirements.txt
23
+
24
+ # Copy the rest of the application's source code
25
+ COPY app.py .
26
+ COPY exercises ./exercises
27
+ COPY pose_estimation ./pose_estimation
28
+ COPY feedback ./feedback
29
+ COPY utils ./utils
30
+ COPY db ./db
31
+ COPY static ./static
32
+ COPY templates ./templates
33
+
34
+ # Make port 8080 available to the world outside this container (Cloud Run default)
35
+ EXPOSE 8080
36
+
37
+ # Define environment variable for the port
38
+ ENV PORT 8080
39
+ ENV MPLCONFIGDIR /tmp/matplotlib_config_cache
40
+
41
+ # Run app.py when the container launches using Gunicorn WSGI server
42
+ CMD ["gunicorn", "--worker-class", "eventlet", "-w", "1", "--bind", "0.0.0.0:${PORT}", "--timeout", "0", "app:app"]
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,255 @@
1
  ---
2
- title: FinalTrain
3
- emoji: 🌖
4
  colorFrom: blue
5
- colorTo: gray
6
  sdk: docker
 
 
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Fitness Trainer Pose Estimation
3
+ emoji: 💪
4
  colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
+ app_file: app.py
8
+ health_check_path: /healthz
9
+ app_port: 8080 # Or the port Gunicorn is set to in your Dockerfile CMD
10
  pinned: false
11
  ---
12
 
13
+ # Fitness Trainer with Pose Estimation
14
+
15
+ An AI-powered web application that tracks exercises using computer vision and provides real-time feedback. This version now includes **real-time feedback via WebSockets** for an enhanced live training experience.
16
+
17
+ ## Features
18
+
19
+ - Real-time pose estimation using MediaPipe.
20
+ - Multiple exercise types: Squats, Push-ups, and Hammer Curls.
21
+ - Customizable sets and repetitions (via UI or API parameters).
22
+ - Exercise form feedback.
23
+ - Progress tracking (if database features are enabled).
24
+ - Web interface for easy access, including a live test page using WebSockets.
25
+ - HTTP API for single frame analysis and stateful exercise tracking.
26
+ - **WebSocket API for low-latency, interactive exercise tracking.**
27
+
28
+ ## Installation
29
+
30
+ 1. Clone the repository:
31
+ ```bash
32
+ git clone https://github.com/yourusername/fitness-trainer-pose-estimation.git # Replace with actual repo URL
33
+ cd fitness-trainer-pose-estimation
34
+ ```
35
+
36
+ 2. Install dependencies:
37
+ ```bash
38
+ pip install -r requirements.txt
39
+ ```
40
+ This will install all necessary Python packages, including Flask, Flask-SocketIO, Gunicorn, eventlet, OpenCV, and MediaPipe.
41
+
42
+ 3. Set up the static folder structure (if not already present):
43
+ ```bash
44
+ mkdir -p static/images
45
+ ```
46
+
47
+ 4. Add exercise images to the `static/images` folder (if using the original UI templates):
48
+ - `squat.png`
49
+ - `push_up.png`
50
+ - `hammer_curl.png`
51
+
52
+ ## Usage
53
+
54
+ ### Running Locally (Development)
55
+
56
+ 1. Start the Flask-SocketIO development server:
57
+ ```bash
58
+ python app.py
59
+ ```
60
+ This runs the app with Flask's built-in server, suitable for development. It will typically be available at `http://127.0.0.1:5000`.
61
+
62
+ 2. Access the application:
63
+ * **Main UI (if developed):** Open a web browser and navigate to `http://127.0.0.1:5000/`.
64
+ * **Live WebSocket Test Page:**
65
+ 1. To use `live_test.html`, it's recommended to serve it via a Flask route. You can add a simple route to `app.py` like this:
66
+ ```python
67
+ @app.route('/live')
68
+ def live_test_page():
69
+ return render_template('live_test.html')
70
+ ```
71
+ And move `live_test.html` into the `templates` folder.
72
+ Then access it via `http://127.0.0.1:5000/live`.
73
+ 2. Alternatively, if you prefer to keep `live_test.html` in the root and test without modifying `app.py` for serving it, you might be able to open the `live_test.html` file directly in your browser. However, ensure the Flask server (`python app.py`) is running, as the JavaScript in `live_test.html` needs to connect to the WebSocket server at `ws://127.0.0.1:5000`. Relative paths to static assets like CSS or JS within the HTML might not work as expected if opened as a direct file, which is why serving it via Flask is more robust.
74
+
75
+ 3. Using the Live Test Page (`live_test.html`):
76
+ * Select an exercise type from the dropdown.
77
+ * Click "Start Trainer".
78
+ * Allow webcam access if prompted.
79
+ * Position yourself in front of your camera so that your full body is visible.
80
+ * Observe the real-time feedback (reps, stage, messages) updated via WebSockets.
81
+ * Click "Stop Trainer" to end the session.
82
+
83
+ ## API Usage
84
+
85
+ The application offers two primary ways to interact with its pose estimation capabilities: a traditional HTTP API and a real-time WebSocket API.
86
+
87
+ ### 1. HTTP API
88
+
89
+ This API is suitable for scenarios where you want to send individual frames or manage exercise sessions via HTTP requests.
90
+
91
+ #### a. Stateless Single-Frame Analysis (`/api/analyze_frame`)
92
+
93
+ Analyzes a single image frame for pose landmarks without maintaining state.
94
+
95
+ * **URL:** `http://127.0.0.1:5000/api/analyze_frame`
96
+ * **Method:** `POST`
97
+ * **Payload (JSON):**
98
+ * `image` (string): Base64 encoded image.
99
+ * `exercise_type` (string, optional): "squat", "push_up", "hammer_curl". Defaults to "squat".
100
+ * **Success Response (JSON):** `{"success": true, "landmarks": [...]}`
101
+ * **Error Response (JSON):** `{"error": "Error message"}`
102
+
103
+ #### b. Stateful HTTP Exercise Tracking (`/api/track_exercise_stream` and `/api/end_exercise_session`)
104
+
105
+ These endpoints allow tracking an exercise over a series of frames, maintaining state on the server.
106
+
107
+ * **`/api/track_exercise_stream` (POST):**
108
+ * **Payload (JSON):**
109
+ * `session_id` (string): Unique ID for the session.
110
+ * `exercise_type` (string): Exercise type.
111
+ * `image` (string): Base64 encoded image of the current frame.
112
+ * `frame_width` (int): Width of the video frame.
113
+ * `frame_height` (int): Height of the video frame.
114
+ * **Success Response (JSON):** Contains `success`, `landmarks_detected`, and exercise-specific `data`.
115
+ * **`/api/end_exercise_session` (POST):**
116
+ * **Payload (JSON):** `session_id` (string).
117
+ * **Success Response (JSON):** Confirmation message.
118
+
119
+ (For detailed request/response structures of the HTTP API, refer to the previous README version or inspect the `app.py` code.)
120
+
121
+ ### 2. WebSocket API for Live Training
122
+
123
+ This API provides a low-latency, bidirectional communication channel for real-time exercise tracking, as used by `live_test.html`.
124
+
125
+ * **Endpoint Connection:** The client connects to the server's root URL, e.g., `ws://127.0.0.1:5000/` (or `wss://your-deployed-app.com/`). Flask-SocketIO handles this on the default namespace.
126
+ * **Client-side Setup:**
127
+ 1. Include the Socket.IO client library in your HTML:
128
+ ```html
129
+ <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
130
+ ```
131
+ 2. Connect to the server:
132
+ ```javascript
133
+ const socket = io(); // Or io.connect('http://your-server-address');
134
+ ```
135
+
136
+ * **Key WebSocket Events:**
137
+
138
+ * **Client to Server:**
139
+ * `socket.emit('start_exercise_session', { exercise_type: 'squat' });`
140
+ * Call this to initiate a new exercise session.
141
+ * `exercise_type` can be 'squat', 'push_up', or 'hammer_curl'.
142
+ * `socket.emit('process_frame', { image: 'base64img_data', exercise_type: 'squat', frame_width: width, frame_height: height });`
143
+ * Send a video frame for processing.
144
+ * `image`: Base64 encoded string of the JPEG frame.
145
+ * `exercise_type`: The current exercise.
146
+ * `frame_width`, `frame_height`: Dimensions of the original video frame.
147
+
148
+ * **Server to Client:**
149
+ * `socket.on('session_started', (data) => { ... });`
150
+ * Confirmation that the session has started.
151
+ * `data` typically includes: `{ session_id: 'server_assigned_sid', exercise_type: 'squat' }`. The `request.sid` on the server is used as the session identifier for WebSocket communications.
152
+ * `socket.on('exercise_update', (data) => { ... });`
153
+ * Provides feedback for the processed frame.
154
+ * `data` structure:
155
+ ```json
156
+ {
157
+ "success": true,
158
+ "landmarks_detected": true, // or false
159
+ "data": {
160
+ // Structure depends on the exercise_type (matches HTTP API's track_exercise_stream response data)
161
+ // Example for 'squat':
162
+ // "counter": 0, "stage": "down", "angle_left": 85.0, "feedback": "Good depth!"
163
+ // Example for 'push_up':
164
+ // "counter": 0, "stage": "up", "angle_body_left": 170.0, "feedback": "Keep body straight."
165
+ // Example for 'hammer_curl':
166
+ // "counter_left": 1, "stage_left": "up", "angle_left_curl": 45.0, "feedback_left": "Good curl!", ... (and right side)
167
+ },
168
+ "message": "Optional message, e.g., 'No landmarks detected.'" // if landmarks_detected is false
169
+ }
170
+ ```
171
+ * `socket.on('session_error', (data) => { ... });`
172
+ * Sent if there's an error starting a session (e.g., invalid exercise type, server busy).
173
+ * `data`: `{ error: "Error message description" }`
174
+ * `socket.on('frame_error', (data) => { ... });`
175
+ * Sent if there's an error processing a specific frame (e.g., bad image data, unexpected server error).
176
+ * `data`: `{ error: "Error message description" }`
177
+ * `socket.on('disconnect', () => { ... });`
178
+ * Standard Socket.IO event. The client is disconnected. Server-side, the `disconnect` handler in `app.py` cleans up the `active_exercise_sessions` entry for that client (`request.sid`).
179
+
180
+ ## Project Structure
181
+
182
+ - `app.py` - Main Flask application with HTTP routes and SocketIO event handlers.
183
+ - `templates/` - HTML templates (e.g., `index.html`, `live_test.html` if moved here).
184
+ - `static/` - CSS, client-side JavaScript (e.g., `websocket_trainer.js`), and images.
185
+ - `pose_estimation/` - Pose estimation modules.
186
+ - `exercises/` - Exercise tracking classes.
187
+ - `feedback/` - User feedback modules.
188
+ - `utils/` - Helper functions and utilities.
189
+ - `Dockerfile` - For building the application container.
190
+ - `requirements.txt` - Python dependencies.
191
+ - `live_test.html` - Client-side example for testing WebSocket functionality (currently in root, consider moving to `templates/` and serving via a route).
192
+
193
+ ## Technologies Used
194
+
195
+ - Flask - Web framework.
196
+ - Flask-SocketIO - For WebSocket communication.
197
+ - Gunicorn + eventlet - For concurrent WSGI server suitable for WebSockets.
198
+ - OpenCV - Computer vision tasks.
199
+ - MediaPipe - Pose estimation.
200
+ - HTML/CSS/JavaScript - Frontend.
201
+
202
+ ## Deployment
203
+
204
+ Containerization with Docker is highly recommended for deployment.
205
+
206
+ ### Dockerfile
207
+
208
+ The provided `Dockerfile` sets up the Python environment, installs dependencies, and configures Gunicorn to run the application.
209
+ **Key change for WebSockets:** The Dockerfile now uses `eventlet` as the worker class for Gunicorn, which is essential for Flask-SocketIO:
210
+ ```dockerfile
211
+ # ... (other Dockerfile instructions) ...
212
+
213
+ # Run app.py when the container launches using Gunicorn WSGI server
214
+ CMD ["gunicorn", "--worker-class", "eventlet", "-w", "1", "--bind", "0.0.0.0:${PORT}", "--timeout", "0", "app:app"]
215
+ ```
216
+ * `--worker-class eventlet`: Uses the asynchronous eventlet worker.
217
+ * `-w 1`: Starts 1 worker process. For eventlet, a single worker can handle many concurrent connections.
218
+ * `--timeout 0`: Disables worker timeouts, important for long-lived WebSocket connections.
219
+
220
+ ### Hosting Options
221
+
222
+ #### 1. Hugging Face Spaces
223
+
224
+ - **Setup:** Push your code (including the `Dockerfile` and `app.py`) to a Hugging Face Space repository. Select "Docker" as the Space SDK.
225
+ - **WebSocket Support:** Hugging Face Spaces generally supports WebSockets. The application should be accessible.
226
+ - **Port Configuration:** Ensure your `CMD` in the Dockerfile uses a port variable like `${PORT}` (which HF Spaces sets, often to 7860 or 8080) or a fixed port that HF Spaces can map. The `app_port: 8080` in the README metadata is a common choice.
227
+
228
+ #### 2. Google Cloud Run
229
+
230
+ - **Setup:** Build your Docker container and push it to Google Container Registry (GCR) or Artifact Registry. Deploy the image from there to Cloud Run.
231
+ - **WebSocket Support & Session Affinity:**
232
+ - Cloud Run supports WebSockets.
233
+ - **Session Affinity:** For applications using Flask-SocketIO with its default in-memory session management (`active_exercise_sessions` in this app, keyed by `request.sid`), session affinity is crucial if you scale to more than one instance. Session affinity directs requests from a specific client to the same container instance. Enable it in your Cloud Run service settings.
234
+ - **Statelessness Consideration:** While session affinity helps, be aware that `active_exercise_sessions` are still tied to individual instances. If an instance restarts or you scale down and up, that specific instance's memory is lost. For more robust session management across multiple instances or instance restarts, an external store (like Redis via Memorystore) for `active_exercise_sessions` would be necessary. For the current implementation, session affinity is the primary mechanism to make WebSockets work with multiple instances.
235
+ - **Gunicorn Command:** The `CMD` in the Dockerfile is already configured for Cloud Run (binds to `0.0.0.0:${PORT}`).
236
+
237
+ #### 3. Other PaaS/VPS
238
+
239
+ - Ensure the platform supports Python and Docker (or allows manual setup of Python, eventlet, Gunicorn).
240
+ - Configure your reverse proxy (like Nginx) correctly to handle WebSocket connections (Upgrade and Connection headers) if you place one in front of Gunicorn.
241
+
242
+ ### Note on Session Management (General)
243
+
244
+ The current application uses an in-memory Python dictionary (`active_exercise_sessions`) to store user session data, keyed by the WebSocket `request.sid`. This is simple but has limitations:
245
+ * **Scalability:** Data is not shared across multiple Gunicorn worker processes (if you were to increase `-w` beyond 1, though with `eventlet` one worker is standard) or multiple server instances (e.g., when load balancing or on platforms like Cloud Run with multiple instances).
246
+ * **Persistence:** Sessions are lost if the server or worker restarts.
247
+ For production environments requiring high availability or scalability, consider implementing session management using an external store like Redis or Memcached.
248
+
249
+ ## Contributing
250
+
251
+ Contributions are welcome! Please feel free to submit a Pull Request. Ensure your contributions align with the project's structure and coding style. If adding new features, please update relevant documentation and consider adding tests.
252
+
253
+ ## License
254
+
255
+ This project is licensed under the MIT License - see the `LICENSE` file for details (if one exists in the repository).
app.py ADDED
@@ -0,0 +1,632 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, Response, request, jsonify, session, redirect, url_for
2
+ from flask_socketio import SocketIO
3
+ import cv2
4
+ import base64
5
+ import io
6
+ import numpy as np
7
+ from PIL import Image
8
+ import threading
9
+ import time
10
+ import uuid
11
+ import sys
12
+ import traceback
13
+ import logging
14
+ from flask_cors import CORS
15
+
16
+ # Set up logging
17
+ logging.basicConfig(level=logging.DEBUG,
18
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
19
+ handlers=[logging.StreamHandler()])
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Import attempt with error handling
23
+ try:
24
+ from pose_estimation.estimation import PoseEstimator
25
+ from exercises.squat import Squat
26
+ from exercises.hammer_curl import HammerCurl
27
+ from exercises.push_up import PushUp
28
+ from feedback.information import get_exercise_info
29
+ from feedback.layout import layout_indicators
30
+ from utils.draw_text_with_background import draw_text_with_background
31
+ logger.info("Successfully imported pose estimation modules")
32
+ except ImportError as e:
33
+ logger.error(f"Failed to import required modules: {e}")
34
+ traceback.print_exc()
35
+ sys.exit(1)
36
+
37
+ # Try to import WorkoutLogger with fallback
38
+ try:
39
+ from db.workout_logger import WorkoutLogger
40
+ workout_logger = WorkoutLogger()
41
+ logger.info("Successfully initialized workout logger")
42
+ except ImportError:
43
+ logger.warning("WorkoutLogger import failed, creating dummy class")
44
+
45
+ class DummyWorkoutLogger:
46
+ def __init__(self):
47
+ pass
48
+ def log_workout(self, *args, **kwargs):
49
+ return {}
50
+ def get_recent_workouts(self, *args, **kwargs):
51
+ return []
52
+ def get_weekly_stats(self, *args, **kwargs):
53
+ return {}
54
+ def get_exercise_distribution(self, *args, **kwargs):
55
+ return {}
56
+ def get_user_stats(self, *args, **kwargs):
57
+ return {'total_workouts': 0, 'total_exercises': 0, 'streak_days': 0}
58
+
59
+ workout_logger = DummyWorkoutLogger()
60
+
61
+ logger.info("Setting up Flask application")
62
+ app = Flask(__name__)
63
+ app.secret_key = 'fitness_trainer_secret_key' # Required for sessions
64
+ CORS(app, origins="*", methods=["GET", "POST", "OPTIONS"], allow_headers=["Content-Type", "Authorization"])
65
+ socketio = SocketIO(app, cors_allowed_origins="*")
66
+
67
+ pose_estimator_api = None
68
+ active_exercise_sessions = {}
69
+ # Max number of concurrent sessions to avoid memory issues
70
+ MAX_SESSIONS = 100
71
+
72
+ def get_pose_estimator_api_instance():
73
+ global pose_estimator_api
74
+ if pose_estimator_api is None:
75
+ logger.info("Initializing PoseEstimator for API")
76
+ pose_estimator_api = PoseEstimator()
77
+ return pose_estimator_api
78
+
79
+ # Global variables
80
+ camera = None
81
+ output_frame = None
82
+ lock = threading.Lock()
83
+ exercise_running = False
84
+ current_exercise = None
85
+ current_exercise_data = None
86
+ exercise_counter = 0
87
+ exercise_goal = 0
88
+ sets_completed = 0
89
+ sets_goal = 0
90
+ workout_start_time = None
91
+
92
+ def initialize_camera():
93
+ global camera
94
+ if camera is None:
95
+ camera = cv2.VideoCapture(0)
96
+ return camera
97
+
98
+ def release_camera():
99
+ global camera
100
+ if camera is not None:
101
+ camera.release()
102
+ camera = None
103
+
104
+ def generate_frames():
105
+ global output_frame, lock, exercise_running, current_exercise, current_exercise_data
106
+ global exercise_counter, exercise_goal, sets_completed, sets_goal
107
+
108
+ pose_estimator = PoseEstimator()
109
+
110
+ while True:
111
+ if camera is None:
112
+ continue
113
+
114
+ success, frame = camera.read()
115
+ if not success:
116
+ continue
117
+
118
+ # Only process frames if an exercise is running
119
+ if exercise_running and current_exercise:
120
+ # Process with pose estimation
121
+ results = pose_estimator.estimate_pose(frame, current_exercise_data['type'])
122
+
123
+ if results.pose_landmarks:
124
+ # Track exercise based on type
125
+ if current_exercise_data['type'] == "squat":
126
+ counter, angle, stage = current_exercise.track_squat(results.pose_landmarks.landmark, frame)
127
+ layout_indicators(frame, current_exercise_data['type'], (counter, angle, stage))
128
+ exercise_counter = counter
129
+
130
+ elif current_exercise_data['type'] == "push_up":
131
+ counter, angle, stage = current_exercise.track_push_up(results.pose_landmarks.landmark, frame)
132
+ layout_indicators(frame, current_exercise_data['type'], (counter, angle, stage))
133
+ exercise_counter = counter
134
+
135
+ elif current_exercise_data['type'] == "hammer_curl":
136
+ (counter_right, angle_right, counter_left, angle_left,
137
+ warning_message_right, warning_message_left, progress_right,
138
+ progress_left, stage_right, stage_left) = current_exercise.track_hammer_curl(
139
+ results.pose_landmarks.landmark, frame)
140
+ layout_indicators(frame, current_exercise_data['type'],
141
+ (counter_right, angle_right, counter_left, angle_left,
142
+ warning_message_right, warning_message_left,
143
+ progress_right, progress_left, stage_right, stage_left))
144
+ exercise_counter = max(counter_right, counter_left)
145
+
146
+ # Display exercise information
147
+ exercise_info = get_exercise_info(current_exercise_data['type'])
148
+ draw_text_with_background(frame, f"Exercise: {exercise_info.get('name', 'N/A')}", (40, 50),
149
+ cv2.FONT_HERSHEY_DUPLEX, 0.7, (255, 255, 255), (118, 29, 14), 1)
150
+ draw_text_with_background(frame, f"Reps Goal: {exercise_goal}", (40, 80),
151
+ cv2.FONT_HERSHEY_DUPLEX, 0.7, (255, 255, 255), (118, 29, 14), 1)
152
+ draw_text_with_background(frame, f"Sets Goal: {sets_goal}", (40, 110),
153
+ cv2.FONT_HERSHEY_DUPLEX, 0.7, (255, 255, 255), (118, 29, 14), 1)
154
+ draw_text_with_background(frame, f"Current Set: {sets_completed + 1}", (40, 140),
155
+ cv2.FONT_HERSHEY_DUPLEX, 0.7, (255, 255, 255), (118, 29, 14), 1)
156
+
157
+ # Check if rep goal is reached for current set
158
+ if exercise_counter >= exercise_goal:
159
+ sets_completed += 1
160
+ exercise_counter = 0
161
+ # Reset exercise counter in the appropriate exercise object
162
+ if current_exercise_data['type'] == "squat" or current_exercise_data['type'] == "push_up":
163
+ current_exercise.counter = 0
164
+ elif current_exercise_data['type'] == "hammer_curl":
165
+ current_exercise.counter_right = 0
166
+ current_exercise.counter_left = 0
167
+
168
+ # Check if all sets are completed
169
+ if sets_completed >= sets_goal:
170
+ exercise_running = False
171
+ draw_text_with_background(frame, "WORKOUT COMPLETE!", (frame.shape[1]//2 - 150, frame.shape[0]//2),
172
+ cv2.FONT_HERSHEY_DUPLEX, 1.2, (255, 255, 255), (0, 200, 0), 2)
173
+ else:
174
+ draw_text_with_background(frame, f"SET {sets_completed} COMPLETE! Rest for 30 sec",
175
+ (frame.shape[1]//2 - 200, frame.shape[0]//2),
176
+ cv2.FONT_HERSHEY_DUPLEX, 1.0, (255, 255, 255), (0, 0, 200), 2)
177
+ # We could add rest timer functionality here
178
+ else:
179
+ # Display welcome message if no exercise is running
180
+ cv2.putText(frame, "Select an exercise to begin", (frame.shape[1]//2 - 150, frame.shape[0]//2),
181
+ cv2.FONT_HERSHEY_DUPLEX, 0.8, (255, 255, 255), 1)
182
+
183
+ # Encode the frame in JPEG format
184
+ with lock:
185
+ output_frame = frame.copy()
186
+
187
+ # Yield the frame in byte format
188
+ ret, buffer = cv2.imencode('.jpg', output_frame)
189
+ frame = buffer.tobytes()
190
+ yield (b'--frame\r\n'
191
+ b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
192
+
193
+ @app.route('/')
194
+ def index():
195
+ """Home page with exercise selection"""
196
+ logger.info("Rendering index page")
197
+ try:
198
+ return render_template('index.html')
199
+ except Exception as e:
200
+ logger.error(f"Error rendering index: {e}")
201
+ return f"Error rendering template: {str(e)}", 500
202
+
203
+ @app.route('/dashboard')
204
+ def dashboard():
205
+ """Dashboard page with workout statistics"""
206
+ logger.info("Rendering dashboard page")
207
+ try:
208
+ # Get data for the dashboard
209
+ recent_workouts = workout_logger.get_recent_workouts(5)
210
+ weekly_stats = workout_logger.get_weekly_stats()
211
+ exercise_distribution = workout_logger.get_exercise_distribution()
212
+ user_stats = workout_logger.get_user_stats()
213
+
214
+ # Format workouts for display
215
+ formatted_workouts = []
216
+ for workout in recent_workouts:
217
+ formatted_workouts.append({
218
+ 'date': workout['date'],
219
+ 'exercise': workout['exercise_type'].replace('_', ' ').title(),
220
+ 'sets': workout['sets'],
221
+ 'reps': workout['reps'],
222
+ 'duration': f"{workout['duration_seconds'] // 60}:{workout['duration_seconds'] % 60:02d}"
223
+ })
224
+
225
+ # Calculate total workouts this week
226
+ weekly_workout_count = sum(day['workout_count'] for day in weekly_stats.values())
227
+
228
+ return render_template('dashboard.html',
229
+ recent_workouts=formatted_workouts,
230
+ weekly_workouts=weekly_workout_count,
231
+ total_workouts=user_stats['total_workouts'],
232
+ total_exercises=user_stats['total_exercises'],
233
+ streak_days=user_stats['streak_days'])
234
+ except Exception as e:
235
+ logger.error(f"Error in dashboard: {e}")
236
+ traceback.print_exc()
237
+ return f"Error loading dashboard: {str(e)}", 500
238
+
239
+ @app.route('/video_feed')
240
+ def video_feed():
241
+ """Video streaming route"""
242
+ return Response(generate_frames(),
243
+ mimetype='multipart/x-mixed-replace; boundary=frame')
244
+
245
+ @app.route('/start_exercise', methods=['POST'])
246
+ def start_exercise():
247
+ """Start a new exercise based on user selection"""
248
+ global exercise_running, current_exercise, current_exercise_data
249
+ global exercise_counter, exercise_goal, sets_completed, sets_goal
250
+ global workout_start_time
251
+
252
+ data = request.json
253
+ exercise_type = data.get('exercise_type')
254
+ sets_goal = int(data.get('sets', 3))
255
+ exercise_goal = int(data.get('reps', 10))
256
+
257
+ # Initialize camera if not already done
258
+ initialize_camera()
259
+
260
+ # Reset counters
261
+ exercise_counter = 0
262
+ sets_completed = 0
263
+ workout_start_time = time.time()
264
+
265
+ # Initialize the appropriate exercise class
266
+ if exercise_type == "squat":
267
+ current_exercise = Squat()
268
+ elif exercise_type == "push_up":
269
+ current_exercise = PushUp()
270
+ elif exercise_type == "hammer_curl":
271
+ current_exercise = HammerCurl()
272
+ else:
273
+ return jsonify({'success': False, 'error': 'Invalid exercise type'})
274
+
275
+ # Store exercise data
276
+ current_exercise_data = {
277
+ 'type': exercise_type,
278
+ 'sets': sets_goal,
279
+ 'reps': exercise_goal
280
+ }
281
+
282
+ # Start the exercise
283
+ exercise_running = True
284
+
285
+ return jsonify({'success': True})
286
+
287
+ @app.route('/stop_exercise', methods=['POST'])
288
+ def stop_exercise():
289
+ """Stop the current exercise and log the workout"""
290
+ global exercise_running, current_exercise_data, workout_start_time
291
+ global exercise_counter, exercise_goal, sets_completed, sets_goal
292
+
293
+ if exercise_running and current_exercise_data:
294
+ # Calculate duration
295
+ duration = int(time.time() - workout_start_time) if workout_start_time else 0
296
+
297
+ # Log the workout
298
+ workout_logger.log_workout(
299
+ exercise_type=current_exercise_data['type'],
300
+ sets=sets_completed + (1 if exercise_counter > 0 else 0), # Include partial set
301
+ reps=exercise_goal,
302
+ duration_seconds=duration
303
+ )
304
+ release_camera()
305
+ exercise_running = False
306
+ return jsonify({'success': True})
307
+
308
+ @app.route('/get_status', methods=['GET'])
309
+ def get_status():
310
+ """Return current exercise status"""
311
+ global exercise_counter, sets_completed, exercise_goal, sets_goal, exercise_running
312
+
313
+ return jsonify({
314
+ 'exercise_running': exercise_running,
315
+ 'current_reps': exercise_counter,
316
+ 'current_set': sets_completed + 1 if exercise_running else 0,
317
+ 'total_sets': sets_goal,
318
+ 'rep_goal': exercise_goal
319
+ })
320
+
321
+ @app.route('/profile')
322
+ def profile():
323
+ """User profile page - placeholder for future development"""
324
+ return "Profile page - Coming soon!"
325
+
326
+ @app.route('/healthz')
327
+ def health_check():
328
+ logger.info("Health check endpoint called successfully.")
329
+ return "OK", 200
330
+
331
+ @app.route('/api/analyze_frame', methods=['POST'])
332
+ def analyze_frame():
333
+ logger.info("API call to /api/analyze_frame")
334
+ try:
335
+ data = request.json
336
+ if not data or 'image' not in data:
337
+ logger.warning("API /api/analyze_frame: No image provided or invalid JSON.")
338
+ return jsonify({'error': 'No image provided in JSON payload (expected base64 string under "image" key)'}), 400
339
+
340
+ # Use 'squat' as a default if not provided. This primarily affects server-side drawing, not the landmarks themselves.
341
+ exercise_type_for_api = data.get('exercise_type', 'squat')
342
+ if exercise_type_for_api not in ["squat", "push_up", "hammer_curl"]: # Validate against known types if necessary
343
+ logger.warning(f"API /api/analyze_frame: Invalid exercise_type '{exercise_type_for_api}'. Defaulting to 'squat'.")
344
+ exercise_type_for_api = "squat"
345
+
346
+ image_data = data['image']
347
+
348
+ # Add padding if missing for base64 decoding
349
+ missing_padding = len(image_data) % 4
350
+ if missing_padding:
351
+ image_data += '=' * (4 - missing_padding)
352
+
353
+ try:
354
+ # Decode the base64 string
355
+ image_bytes = base64.b64decode(image_data)
356
+ pil_image = Image.open(io.BytesIO(image_bytes))
357
+ # Convert PIL image (RGB) to OpenCV frame (BGR)
358
+ frame = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
359
+ except Exception as e:
360
+ logger.error(f"API /api/analyze_frame: Error decoding base64 image: {e}")
361
+ return jsonify({'error': f'Invalid base64 image data: {str(e)}'}), 400
362
+
363
+ estimator = get_pose_estimator_api_instance()
364
+
365
+ # Process with pose estimation
366
+ results = estimator.estimate_pose(frame, exercise_type_for_api)
367
+
368
+ landmarks_list = []
369
+ if results.pose_landmarks:
370
+ for i, landmark in enumerate(results.pose_landmarks.landmark):
371
+ landmarks_list.append({
372
+ 'index': i,
373
+ 'x': landmark.x,
374
+ 'y': landmark.y,
375
+ 'z': landmark.z,
376
+ 'visibility': landmark.visibility if hasattr(landmark, 'visibility') else None
377
+ })
378
+ logger.info(f"API /api/analyze_frame: Successfully processed image, found {len(landmarks_list)} landmarks for exercise type '{exercise_type_for_api}'.")
379
+ return jsonify({'success': True, 'landmarks': landmarks_list})
380
+ else:
381
+ logger.info(f"API /api/analyze_frame: No landmarks detected in the provided image for exercise type '{exercise_type_for_api}'.")
382
+ return jsonify({'success': True, 'landmarks': []})
383
+
384
+ except Exception as e:
385
+ logger.error(f"API /api/analyze_frame: Error: {e}")
386
+ traceback.print_exc()
387
+ return jsonify({'error': f'Internal server error: {str(e)}'}), 500
388
+
389
+ @app.route('/api/track_exercise_stream', methods=['POST'])
390
+ def track_exercise_stream():
391
+ logger.info("API call to /api/track_exercise_stream")
392
+ data = {} # Initialize data to ensure it's defined for logging in case of early errors
393
+ try:
394
+ data = request.json
395
+ if not data:
396
+ return jsonify({'error': 'No JSON data provided'}), 400
397
+
398
+ session_id = data.get('session_id')
399
+ exercise_type = data.get('exercise_type')
400
+ image_data_base64 = data.get('image')
401
+ frame_width = data.get('frame_width')
402
+ frame_height = data.get('frame_height')
403
+
404
+ if not all([session_id, exercise_type, image_data_base64, frame_width, frame_height]):
405
+ return jsonify({'error': 'Missing required fields: session_id, exercise_type, image, frame_width, frame_height'}), 400
406
+
407
+ if not isinstance(frame_width, int) or not isinstance(frame_height, int) or frame_width <= 0 or frame_height <= 0:
408
+ return jsonify({'error': 'Invalid frame_width or frame_height'}), 400
409
+
410
+ # Manage session limit
411
+ if len(active_exercise_sessions) >= MAX_SESSIONS and session_id not in active_exercise_sessions:
412
+ logger.warning(f"Max sessions ({MAX_SESSIONS}) reached. Rejecting new session {session_id}.")
413
+ # Optional: could try to evict oldest session
414
+ return jsonify({'error': 'Server busy, max sessions reached. Please try again later.'}), 503
415
+
416
+
417
+ # Get or create exercise session object
418
+ if session_id not in active_exercise_sessions:
419
+ logger.info(f"Creating new session {session_id} for exercise {exercise_type}")
420
+ if exercise_type == 'squat':
421
+ active_exercise_sessions[session_id] = Squat()
422
+ elif exercise_type == 'push_up':
423
+ active_exercise_sessions[session_id] = PushUp()
424
+ elif exercise_type == 'hammer_curl':
425
+ active_exercise_sessions[session_id] = HammerCurl()
426
+ else:
427
+ logger.warning(f"Invalid exercise type: {exercise_type} for session {session_id}")
428
+ return jsonify({'error': 'Invalid exercise_type'}), 400
429
+
430
+ exercise_session = active_exercise_sessions[session_id]
431
+
432
+ # Decode image (similar to /api/analyze_frame)
433
+ missing_padding = len(image_data_base64) % 4
434
+ if missing_padding:
435
+ image_data_base64 += '=' * (4 - missing_padding)
436
+ try:
437
+ image_bytes = base64.b64decode(image_data_base64)
438
+ pil_image = Image.open(io.BytesIO(image_bytes))
439
+ frame_for_estimation = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR) # For PoseEstimator
440
+ except Exception as e:
441
+ logger.error(f"Session {session_id}: Error decoding base64 image: {e}")
442
+ return jsonify({'error': f'Invalid base64 image data: {str(e)}'}), 400
443
+
444
+ # Get pose landmarks
445
+ pose_estimator = get_pose_estimator_api_instance() # Reusing the existing estimator instance getter
446
+ # The 'squat' here is just a default for PoseEstimator's internal drawing logic, which we don't use for API response.
447
+ # The actual exercise type for tracking is handled by `exercise_session` object.
448
+ results = pose_estimator.estimate_pose(frame_for_estimation, 'squat')
449
+
450
+ if not results.pose_landmarks:
451
+ logger.info(f"Session {session_id}: No landmarks detected.")
452
+ # Return current state even if no new landmarks, or specific message
453
+ return jsonify({
454
+ 'success': True,
455
+ 'landmarks_detected': False,
456
+ 'message': 'No landmarks detected in this frame.',
457
+ # Optionally, could return last known state from exercise_session if needed
458
+ })
459
+
460
+ exercise_data = None
461
+ if exercise_type == 'squat':
462
+ exercise_data = exercise_session.track_squat(results.pose_landmarks.landmark, frame_width, frame_height)
463
+ elif exercise_type == 'push_up':
464
+ exercise_data = exercise_session.track_push_up(results.pose_landmarks.landmark, frame_width, frame_height)
465
+ elif exercise_type == 'hammer_curl':
466
+ exercise_data = exercise_session.track_hammer_curl(results.pose_landmarks.landmark, frame_width, frame_height)
467
+
468
+ if exercise_data:
469
+ logger.debug(f"Session {session_id}: Exercise data: {exercise_data}")
470
+ return jsonify({'success': True, 'landmarks_detected': True, 'data': exercise_data})
471
+ else:
472
+ # Should not happen if exercise_type is valid and session initialized
473
+ logger.error(f"Session {session_id}: Could not get exercise_data for {exercise_type}")
474
+ return jsonify({'error': 'Failed to process exercise frame.'}), 500
475
+
476
+ except Exception as e:
477
+ session_id_log = data.get('session_id', 'unknown_session') if isinstance(data, dict) else 'unknown_session'
478
+ logger.error(f"API /api/track_exercise_stream Error for session {session_id_log}: {e}")
479
+ traceback.print_exc()
480
+ return jsonify({'error': f'Internal server error: {str(e)}'}), 500
481
+
482
+ @app.route('/api/end_exercise_session', methods=['POST'])
483
+ def end_exercise_session():
484
+ logger.info("API call to /api/end_exercise_session")
485
+ try:
486
+ data = request.json
487
+ if not data:
488
+ return jsonify({'error': 'No JSON data provided'}), 400
489
+
490
+ session_id = data.get('session_id')
491
+ if not session_id:
492
+ return jsonify({'error': 'Missing session_id'}), 400
493
+
494
+ if session_id in active_exercise_sessions:
495
+ del active_exercise_sessions[session_id]
496
+ logger.info(f"Ended and removed session: {session_id}")
497
+ return jsonify({'success': True, 'message': f'Session {session_id} ended.'})
498
+ else:
499
+ logger.warning(f"Attempted to end non-existent session: {session_id}")
500
+ return jsonify({'success': False, 'message': f'Session {session_id} not found.'}), 404
501
+ except Exception as e:
502
+ logger.error(f"API /api/end_exercise_session Error: {e}")
503
+ traceback.print_exc()
504
+ return jsonify({'error': f'Internal server error: {str(e)}'}), 500
505
+
506
+ # SocketIO Event Handlers
507
+
508
+ @socketio.on('connect')
509
+ def handle_connect():
510
+ logger.info(f"Client connected: {request.sid}")
511
+ socketio.emit('connection_ack', {'message': 'Connected to server', 'sid': request.sid}, room=request.sid)
512
+
513
+ @socketio.on('disconnect')
514
+ def handle_disconnect():
515
+ logger.info(f"Client disconnected: {request.sid}")
516
+ if request.sid in active_exercise_sessions:
517
+ del active_exercise_sessions[request.sid]
518
+ logger.info(f"Cleaned up exercise session for disconnected client {request.sid}")
519
+
520
+ @socketio.on('start_exercise_session')
521
+ def handle_start_exercise_session(data):
522
+ exercise_type = data.get('exercise_type')
523
+ logger.info(f"Attempting to start exercise session for {request.sid} with type: {exercise_type}")
524
+
525
+ if not exercise_type:
526
+ logger.warning(f"Session start for {request.sid} failed: no exercise_type provided.")
527
+ socketio.emit('session_error', {'error': 'exercise_type is required.'}, room=request.sid)
528
+ return
529
+
530
+ if len(active_exercise_sessions) >= MAX_SESSIONS and request.sid not in active_exercise_sessions:
531
+ logger.warning(f"Max sessions ({MAX_SESSIONS}) reached. Rejecting new session for {request.sid}.")
532
+ socketio.emit('session_error', {'error': 'Server busy, max sessions reached.'}, room=request.sid)
533
+ return
534
+
535
+ if request.sid in active_exercise_sessions:
536
+ logger.info(f"Session for {request.sid} already exists. Re-initializing for new exercise: {exercise_type}")
537
+ # Optionally, could end the previous exercise type cleanly if needed
538
+ # For now, just overwrite with the new one or ensure client manages this flow.
539
+
540
+ if exercise_type == 'squat':
541
+ active_exercise_sessions[request.sid] = {'exercise': Squat(), 'type': exercise_type}
542
+ elif exercise_type == 'push_up':
543
+ active_exercise_sessions[request.sid] = {'exercise': PushUp(), 'type': exercise_type}
544
+ elif exercise_type == 'hammer_curl':
545
+ active_exercise_sessions[request.sid] = {'exercise': HammerCurl(), 'type': exercise_type}
546
+ else:
547
+ logger.warning(f"Invalid exercise type: {exercise_type} for session {request.sid}")
548
+ socketio.emit('session_error', {'error': 'Invalid exercise_type'}, room=request.sid)
549
+ return
550
+
551
+ logger.info(f"Successfully created exercise session for {request.sid}, type: {exercise_type}")
552
+ socketio.emit('session_started', {'session_id': request.sid, 'exercise_type': exercise_type}, room=request.sid)
553
+
554
+ @socketio.on('process_frame')
555
+ def handle_process_frame(data):
556
+ # logger.debug(f"Received frame from {request.sid} for processing.") # Too verbose for every frame
557
+ if request.sid not in active_exercise_sessions:
558
+ logger.warning(f"Frame received from {request.sid} without an active session.")
559
+ socketio.emit('frame_error', {'error': 'No active session. Please start an exercise session first.'}, room=request.sid)
560
+ return
561
+
562
+ session_info = active_exercise_sessions.get(request.sid)
563
+ exercise_session = session_info['exercise']
564
+ session_exercise_type = session_info['type']
565
+
566
+ image_data_base64 = data.get('image')
567
+ frame_width = data.get('frame_width')
568
+ frame_height = data.get('frame_height')
569
+ # client_exercise_type = data.get('exercise_type') # Client sends this, ensure it matches session
570
+
571
+ # if client_exercise_type != session_exercise_type:
572
+ # logger.warning(f"Mismatched exercise type from client {request.sid}. Expected {session_exercise_type}, got {client_exercise_type}")
573
+ # socketio.emit('frame_error', {'error': f'Mismatched exercise type. Current session is for {session_exercise_type}.'}, room=request.sid)
574
+ # return
575
+
576
+ if not all([image_data_base64, frame_width, frame_height]):
577
+ logger.warning(f"Missing data in process_frame for {request.sid}: image, frame_width, or frame_height missing.")
578
+ socketio.emit('frame_error', {'error': 'Missing image, frame_width, or frame_height.'}, room=request.sid)
579
+ return
580
+
581
+ try:
582
+ missing_padding = len(image_data_base64) % 4
583
+ if missing_padding:
584
+ image_data_base64 += '=' * (4 - missing_padding)
585
+
586
+ image_bytes = base64.b64decode(image_data_base64)
587
+ pil_image = Image.open(io.BytesIO(image_bytes))
588
+ frame_for_estimation = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
589
+ except Exception as e:
590
+ logger.error(f"Error decoding base64 image for {request.sid}: {e}")
591
+ socketio.emit('frame_error', {'error': f'Invalid base64 image data: {str(e)}'}, room=request.sid)
592
+ return
593
+
594
+ pose_estimator = get_pose_estimator_api_instance()
595
+ # The 'squat' parameter to estimate_pose is a default for drawing, actual logic is in exercise_session
596
+ results = pose_estimator.estimate_pose(frame_for_estimation, session_exercise_type)
597
+
598
+ exercise_data_result = None
599
+ if results.pose_landmarks:
600
+ if session_exercise_type == 'squat':
601
+ exercise_data_result = exercise_session.track_squat(results.pose_landmarks.landmark, frame_width, frame_height)
602
+ elif session_exercise_type == 'push_up':
603
+ exercise_data_result = exercise_session.track_push_up(results.pose_landmarks.landmark, frame_width, frame_height)
604
+ elif session_exercise_type == 'hammer_curl':
605
+ exercise_data_result = exercise_session.track_hammer_curl(results.pose_landmarks.landmark, frame_width, frame_height)
606
+
607
+ if exercise_data_result:
608
+ # logger.debug(f"Exercise data for {request.sid}: {exercise_data_result}")
609
+ socketio.emit('exercise_update', {'success': True, 'landmarks_detected': True, 'data': exercise_data_result}, room=request.sid)
610
+ else:
611
+ # This case should ideally not be reached if landmarks are detected and exercise type is valid
612
+ logger.error(f"Could not get exercise data for {request.sid}, type {session_exercise_type}, despite landmarks.")
613
+ socketio.emit('exercise_update', {'success': False, 'landmarks_detected': True, 'message': 'Error processing landmarks.'}, room=request.sid)
614
+ else:
615
+ # logger.info(f"No landmarks detected for {request.sid}.")
616
+ socketio.emit('exercise_update', {'success': True, 'landmarks_detected': False, 'message': 'No landmarks detected.'}, room=request.sid)
617
+
618
+ except Exception as e:
619
+ logger.error(f"Error processing frame for {request.sid}: {e}")
620
+ traceback.print_exc()
621
+ socketio.emit('frame_error', {'error': f'Internal server error during frame processing: {str(e)}'}, room=request.sid)
622
+
623
+
624
+ if __name__ == '__main__':
625
+ try:
626
+ logger.info("Starting the Flask application on http://127.0.0.1:5000")
627
+ print("Starting Fitness Trainer app, please wait...")
628
+ print("Open http://127.0.0.1:5000 in your web browser when the server starts")
629
+ socketio.run(app, debug=True, host='0.0.0.0', port=5000)
630
+ except Exception as e:
631
+ logger.error(f"Failed to start application: {e}")
632
+ 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/__init__.py ADDED
File without changes
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/__init__.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,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mediapipe as mp
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 = "up" # Changed from "Initial"
9
+ self.angle_threshold_up = 150
10
+ self.angle_threshold_down = 70
11
+ self.last_counter_update = time.time()
12
+ self.mp_pose = mp.solutions.pose # Added
13
+
14
+ def calculate_shoulder_elbow_wrist_angle(self, shoulder, elbow, wrist):
15
+ """Calculate the angle between shoulder, elbow, and wrist."""
16
+ return calculate_angle(shoulder, elbow, wrist)
17
+
18
+ def track_push_up(self, landmarks_mp, frame_width, frame_height):
19
+ lm = landmarks_mp # shortcut
20
+
21
+ # Left side landmarks
22
+ shoulder_left = [int(lm[self.mp_pose.PoseLandmark.LEFT_SHOULDER.value].x * frame_width),
23
+ int(lm[self.mp_pose.PoseLandmark.LEFT_SHOULDER.value].y * frame_height)]
24
+ elbow_left = [int(lm[self.mp_pose.PoseLandmark.LEFT_ELBOW.value].x * frame_width),
25
+ int(lm[self.mp_pose.PoseLandmark.LEFT_ELBOW.value].y * frame_height)]
26
+ wrist_left = [int(lm[self.mp_pose.PoseLandmark.LEFT_WRIST.value].x * frame_width),
27
+ int(lm[self.mp_pose.PoseLandmark.LEFT_WRIST.value].y * frame_height)]
28
+
29
+ # Right side landmarks
30
+ shoulder_right = [int(lm[self.mp_pose.PoseLandmark.RIGHT_SHOULDER.value].x * frame_width),
31
+ int(lm[self.mp_pose.PoseLandmark.RIGHT_SHOULDER.value].y * frame_height)]
32
+ elbow_right = [int(lm[self.mp_pose.PoseLandmark.RIGHT_ELBOW.value].x * frame_width),
33
+ int(lm[self.mp_pose.PoseLandmark.RIGHT_ELBOW.value].y * frame_height)]
34
+ wrist_right = [int(lm[self.mp_pose.PoseLandmark.RIGHT_WRIST.value].x * frame_width),
35
+ int(lm[self.mp_pose.PoseLandmark.RIGHT_WRIST.value].y * frame_height)]
36
+
37
+ # Calculate angles
38
+ angle_left = self.calculate_shoulder_elbow_wrist_angle(shoulder_left, elbow_left, wrist_left)
39
+ angle_right = self.calculate_shoulder_elbow_wrist_angle(shoulder_right, elbow_right, wrist_right)
40
+
41
+ # Stage and Counter Logic
42
+ current_angle_for_logic = angle_left
43
+ current_time = time.time()
44
+
45
+ if current_angle_for_logic > self.angle_threshold_up:
46
+ self.stage = "up"
47
+ elif self.angle_threshold_down < current_angle_for_logic < self.angle_threshold_up and self.stage == "up":
48
+ self.stage = "down"
49
+ elif current_angle_for_logic < self.angle_threshold_down and self.stage == "down":
50
+ if current_time - self.last_counter_update > 1: # 1 second debounce
51
+ self.counter += 1
52
+ self.last_counter_update = current_time
53
+ self.stage = "up" # Transition back to up
54
+
55
+ feedback = self._get_push_up_feedback(angle_left, angle_right, self.stage)
56
+
57
+ return {
58
+ "counter": self.counter,
59
+ "stage": self.stage,
60
+ "angle_left": angle_left,
61
+ "angle_right": angle_right,
62
+ "feedback": feedback
63
+ }
64
+
65
+ def _get_push_up_feedback(self, angle_left, angle_right, stage):
66
+ feedback = "Keep going!" # Default
67
+
68
+ if stage == "down":
69
+ if min(angle_left, angle_right) < self.angle_threshold_down - 5:
70
+ feedback = "Good depth!"
71
+ elif min(angle_left, angle_right) > self.angle_threshold_down + 10:
72
+ feedback = "Go lower."
73
+ elif stage == "up":
74
+ feedback = "Push up!" # Or "Ready"
75
+
76
+ if abs(angle_left - angle_right) > 25:
77
+ feedback += " Try to keep your push-up even." if feedback != "Keep going!" else "Try to keep your push-up even."
78
+
79
+ return feedback.strip()
80
+
81
+ def get_drawing_annotations(self, landmarks_mp, frame_width, frame_height, exercise_data_dict):
82
+ annotations = []
83
+ lm = landmarks_mp # shortcut
84
+
85
+ # Pixel coordinates
86
+ shoulder_left = [int(lm[self.mp_pose.PoseLandmark.LEFT_SHOULDER.value].x * frame_width),
87
+ int(lm[self.mp_pose.PoseLandmark.LEFT_SHOULDER.value].y * frame_height)]
88
+ elbow_left = [int(lm[self.mp_pose.PoseLandmark.LEFT_ELBOW.value].x * frame_width),
89
+ int(lm[self.mp_pose.PoseLandmark.LEFT_ELBOW.value].y * frame_height)]
90
+ wrist_left = [int(lm[self.mp_pose.PoseLandmark.LEFT_WRIST.value].x * frame_width),
91
+ int(lm[self.mp_pose.PoseLandmark.LEFT_WRIST.value].y * frame_height)]
92
+
93
+ shoulder_right = [int(lm[self.mp_pose.PoseLandmark.RIGHT_SHOULDER.value].x * frame_width),
94
+ int(lm[self.mp_pose.PoseLandmark.RIGHT_SHOULDER.value].y * frame_height)]
95
+ elbow_right = [int(lm[self.mp_pose.PoseLandmark.RIGHT_ELBOW.value].x * frame_width),
96
+ int(lm[self.mp_pose.PoseLandmark.RIGHT_ELBOW.value].y * frame_height)]
97
+ wrist_right = [int(lm[self.mp_pose.PoseLandmark.RIGHT_WRIST.value].x * frame_width),
98
+ int(lm[self.mp_pose.PoseLandmark.RIGHT_WRIST.value].y * frame_height)]
99
+
100
+ # Lines (original colors: left (0,0,255) -> BGR [255,0,0], right (102,0,0) -> BGR [0,0,102])
101
+ annotations.append({"type": "line", "start_point": shoulder_left, "end_point": elbow_left, "color_bgr": [255, 0, 0], "thickness": 2})
102
+ annotations.append({"type": "line", "start_point": elbow_left, "end_point": wrist_left, "color_bgr": [255, 0, 0], "thickness": 2})
103
+ annotations.append({"type": "line", "start_point": shoulder_right, "end_point": elbow_right, "color_bgr": [0, 0, 102], "thickness": 2})
104
+ annotations.append({"type": "line", "start_point": elbow_right, "end_point": wrist_right, "color_bgr": [0, 0, 102], "thickness": 2})
105
+
106
+ # Circles
107
+ annotations.append({"type": "circle", "center_point": shoulder_left, "radius": 8, "color_bgr": [255, 0, 0], "filled": True})
108
+ annotations.append({"type": "circle", "center_point": elbow_left, "radius": 8, "color_bgr": [255, 0, 0], "filled": True})
109
+ annotations.append({"type": "circle", "center_point": wrist_left, "radius": 8, "color_bgr": [255, 0, 0], "filled": True})
110
+ annotations.append({"type": "circle", "center_point": shoulder_right, "radius": 8, "color_bgr": [0, 0, 102], "filled": True})
111
+ annotations.append({"type": "circle", "center_point": elbow_right, "radius": 8, "color_bgr": [0, 0, 102], "filled": True})
112
+ annotations.append({"type": "circle", "center_point": wrist_right, "radius": 8, "color_bgr": [0, 0, 102], "filled": True})
113
+
114
+ # Text for angles
115
+ if 'angle_left' in exercise_data_dict:
116
+ annotations.append({"type": "text", "text_content": f"Angle L: {int(exercise_data_dict['angle_left'])}",
117
+ "position": [elbow_left[0] + 10, elbow_left[1] - 10],
118
+ "font_scale": 0.5, "color_bgr": [255, 255, 255], "thickness": 2})
119
+ if 'angle_right' in exercise_data_dict:
120
+ annotations.append({"type": "text", "text_content": f"Angle R: {int(exercise_data_dict['angle_right'])}",
121
+ "position": [elbow_right[0] + 10, elbow_right[1] - 10],
122
+ "font_scale": 0.5, "color_bgr": [255, 255, 255], "thickness": 2})
123
+
124
+ # Display main feedback from exercise_data_dict
125
+ if 'feedback' in exercise_data_dict:
126
+ annotations.append({"type": "text", "text_content": exercise_data_dict['feedback'],
127
+ "position": [frame_width // 2 - 150, frame_height - 40], # Adjusted for longer text
128
+ "font_scale": 0.7, "color_bgr": [0, 255, 0], "thickness": 2}) # Green for feedback
129
+
130
+ return annotations
exercises/squat.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mediapipe as mp
2
+ from pose_estimation.angle_calculation import calculate_angle
3
+
4
+ class Squat:
5
+ def __init__(self):
6
+ self.counter = 0
7
+ self.stage = "up" # Initial stage
8
+ self.mp_pose = mp.solutions.pose # Added for convenience
9
+
10
+ def calculate_angle(self, point1, point2, point3): # Assuming these are pixel coordinates
11
+ return calculate_angle(point1, point2, point3)
12
+
13
+ def track_squat(self, landmarks_mp, frame_width, frame_height):
14
+ lm = landmarks_mp # shortcut
15
+
16
+ # Left side landmarks
17
+ shoulder_left = [int(lm[self.mp_pose.PoseLandmark.LEFT_SHOULDER.value].x * frame_width),
18
+ int(lm[self.mp_pose.PoseLandmark.LEFT_SHOULDER.value].y * frame_height)]
19
+ hip_left = [int(lm[self.mp_pose.PoseLandmark.LEFT_HIP.value].x * frame_width),
20
+ int(lm[self.mp_pose.PoseLandmark.LEFT_HIP.value].y * frame_height)]
21
+ knee_left = [int(lm[self.mp_pose.PoseLandmark.LEFT_KNEE.value].x * frame_width),
22
+ int(lm[self.mp_pose.PoseLandmark.LEFT_KNEE.value].y * frame_height)]
23
+ # ankle_left = [int(lm[self.mp_pose.PoseLandmark.LEFT_ANKLE.value].x * frame_width),
24
+ # int(lm[self.mp_pose.PoseLandmark.LEFT_ANKLE.value].y * frame_height)]
25
+
26
+
27
+ # Right side landmarks
28
+ shoulder_right = [int(lm[self.mp_pose.PoseLandmark.RIGHT_SHOULDER.value].x * frame_width),
29
+ int(lm[self.mp_pose.PoseLandmark.RIGHT_SHOULDER.value].y * frame_height)]
30
+ hip_right = [int(lm[self.mp_pose.PoseLandmark.RIGHT_HIP.value].x * frame_width),
31
+ int(lm[self.mp_pose.PoseLandmark.RIGHT_HIP.value].y * frame_height)]
32
+ knee_right = [int(lm[self.mp_pose.PoseLandmark.RIGHT_KNEE.value].x * frame_width),
33
+ int(lm[self.mp_pose.PoseLandmark.RIGHT_KNEE.value].y * frame_height)]
34
+ # ankle_right = [int(lm[self.mp_pose.PoseLandmark.RIGHT_ANKLE.value].x * frame_width),
35
+ # int(lm[self.mp_pose.PoseLandmark.RIGHT_ANKLE.value].y * frame_height)]
36
+
37
+ # Calculate angles
38
+ angle_left = self.calculate_angle(shoulder_left, hip_left, knee_left)
39
+ angle_right = self.calculate_angle(shoulder_right, hip_right, knee_right)
40
+
41
+ # Stage and counter logic (using angle_left for primary logic)
42
+ current_angle_for_logic = angle_left
43
+ if current_angle_for_logic > 170:
44
+ self.stage = "up"
45
+ elif 90 < current_angle_for_logic < 170 and self.stage == "up":
46
+ self.stage = "down"
47
+ elif current_angle_for_logic < 90 and self.stage == "down":
48
+ self.stage = "up"
49
+ self.counter += 1
50
+
51
+ feedback_message = self._get_squat_feedback(angle_left, angle_right, self.stage,
52
+ knee_left, hip_left, shoulder_left,
53
+ knee_right, hip_right, shoulder_right)
54
+
55
+ return {
56
+ "counter": self.counter,
57
+ "stage": self.stage,
58
+ "angle_left": angle_left,
59
+ "angle_right": angle_right,
60
+ "feedback": feedback_message
61
+ }
62
+
63
+ def _get_squat_feedback(self, angle_left, angle_right, stage,
64
+ knee_left, hip_left, shoulder_left,
65
+ knee_right, hip_right, shoulder_right): # Added points for future use
66
+ feedback = "Keep going." # Default feedback
67
+
68
+ if stage == "down":
69
+ if min(angle_left, angle_right) < 80:
70
+ feedback = "Good depth!"
71
+ elif min(angle_left, angle_right) > 100: # Knees should be more bent
72
+ feedback = "Go lower."
73
+
74
+ if abs(angle_left - angle_right) > 20: # Check for uneven squat
75
+ # Adding a check to see if there's significant movement, e.g., not in "up" stage fully extended
76
+ if not (stage == "up" and min(angle_left, angle_right) > 160): # Avoid this message if standing straight
77
+ feedback += " Try to keep your squat even." if feedback != "Keep going." else "Try to keep your squat even."
78
+
79
+
80
+ # Placeholder for more advanced feedback using the passed points:
81
+ # E.g., Knee valgus: check if knee_left.x < hip_left.x and knee_left.x > shoulder_left.x (simplified)
82
+ # E.g., Back posture: calculate angle shoulder-hip-ankle (requires ankle points)
83
+
84
+ return feedback.strip()
85
+
86
+
87
+ def get_drawing_annotations(self, landmarks_mp, frame_width, frame_height, exercise_data_dict):
88
+ annotations = []
89
+ lm = landmarks_mp # shortcut
90
+
91
+ # Re-calculate or retrieve necessary points (pixel coordinates)
92
+ # For simplicity, re-calculating here. Could be optimized by passing from track_squat.
93
+ shoulder_left = [int(lm[self.mp_pose.PoseLandmark.LEFT_SHOULDER.value].x * frame_width),
94
+ int(lm[self.mp_pose.PoseLandmark.LEFT_SHOULDER.value].y * frame_height)]
95
+ hip_left = [int(lm[self.mp_pose.PoseLandmark.LEFT_HIP.value].x * frame_width),
96
+ int(lm[self.mp_pose.PoseLandmark.LEFT_HIP.value].y * frame_height)]
97
+ knee_left = [int(lm[self.mp_pose.PoseLandmark.LEFT_KNEE.value].x * frame_width),
98
+ int(lm[self.mp_pose.PoseLandmark.LEFT_KNEE.value].y * frame_height)]
99
+
100
+ shoulder_right = [int(lm[self.mp_pose.PoseLandmark.RIGHT_SHOULDER.value].x * frame_width),
101
+ int(lm[self.mp_pose.PoseLandmark.RIGHT_SHOULDER.value].y * frame_height)]
102
+ hip_right = [int(lm[self.mp_pose.PoseLandmark.RIGHT_HIP.value].x * frame_width),
103
+ int(lm[self.mp_pose.PoseLandmark.RIGHT_HIP.value].y * frame_height)]
104
+ knee_right = [int(lm[self.mp_pose.PoseLandmark.RIGHT_KNEE.value].x * frame_width),
105
+ int(lm[self.mp_pose.PoseLandmark.RIGHT_KNEE.value].y * frame_height)]
106
+
107
+ # Lines for left side (original color: (178, 102, 255) -> BGR: [255, 102, 178])
108
+ annotations.append({"type": "line", "start_point": shoulder_left, "end_point": hip_left, "color_bgr": [255, 102, 178], "thickness": 2})
109
+ annotations.append({"type": "line", "start_point": hip_left, "end_point": knee_left, "color_bgr": [255, 102, 178], "thickness": 2})
110
+
111
+ # Lines for right side (original color: (51, 153, 255) -> BGR: [255, 153, 51])
112
+ annotations.append({"type": "line", "start_point": shoulder_right, "end_point": hip_right, "color_bgr": [255, 153, 51], "thickness": 2})
113
+ annotations.append({"type": "line", "start_point": hip_right, "end_point": knee_right, "color_bgr": [255, 153, 51], "thickness": 2})
114
+
115
+ # Circles for left side
116
+ annotations.append({"type": "circle", "center_point": shoulder_left, "radius": 8, "color_bgr": [255, 102, 178], "filled": True})
117
+ annotations.append({"type": "circle", "center_point": hip_left, "radius": 8, "color_bgr": [255, 102, 178], "filled": True})
118
+ annotations.append({"type": "circle", "center_point": knee_left, "radius": 8, "color_bgr": [255, 102, 178], "filled": True})
119
+
120
+ # Circles for right side
121
+ annotations.append({"type": "circle", "center_point": shoulder_right, "radius": 8, "color_bgr": [255, 153, 51], "filled": True})
122
+ annotations.append({"type": "circle", "center_point": hip_right, "radius": 8, "color_bgr": [255, 153, 51], "filled": True})
123
+ annotations.append({"type": "circle", "center_point": knee_right, "radius": 8, "color_bgr": [255, 153, 51], "filled": True})
124
+
125
+ # Text for angles
126
+ if 'angle_left' in exercise_data_dict:
127
+ annotations.append({"type": "text", "text_content": f"Angle L: {int(exercise_data_dict['angle_left'])}",
128
+ "position": [knee_left[0] + 10, knee_left[1] - 10],
129
+ "font_scale": 0.5, "color_bgr": [255, 255, 255], "thickness": 2})
130
+ if 'angle_right' in exercise_data_dict:
131
+ annotations.append({"type": "text", "text_content": f"Angle R: {int(exercise_data_dict['angle_right'])}",
132
+ "position": [knee_right[0] + 10, knee_right[1] - 10],
133
+ "font_scale": 0.5, "color_bgr": [255, 255, 255], "thickness": 2})
134
+
135
+ # Display main feedback from exercise_data_dict
136
+ if 'feedback' in exercise_data_dict:
137
+ annotations.append({"type": "text", "text_content": exercise_data_dict['feedback'],
138
+ "position": [frame_width // 2 - 100, frame_height - 40], # Centered at bottom
139
+ "font_scale": 0.7, "color_bgr": [0, 0, 255], "thickness": 2})
140
+
141
+
142
+ return annotations
feedback/__init__.py ADDED
File without changes
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
+
live_test.html ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Live Fitness Trainer Test (WebSocket)</title>
7
+ <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
8
+ <style>
9
+ body { font-family: sans-serif; display: flex; flex-direction: column; align-items: center; margin: 0; padding: 20px; background-color: #f4f4f4; }
10
+ #controls { margin-bottom: 20px; padding: 15px; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
11
+ label, select, button { font-size: 1em; margin: 5px; }
12
+ button { padding: 10px 15px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
13
+ button:disabled { background-color: #ccc; }
14
+ button:hover:not(:disabled) { background-color: #0056b3; }
15
+ #videoContainer { display: flex; flex-wrap: wrap; justify-content: center; gap: 20px; margin-bottom: 20px; }
16
+ video { border: 2px solid #007bff; transform: scaleX(-1); border-radius: 8px; background-color: #000; } /* Flip video for mirror effect */
17
+ #feedbackArea { border: 1px solid #ccc; padding: 15px; width: 320px; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
18
+ #feedbackArea h3 { margin-top: 0; color: #007bff; }
19
+ #feedbackArea p { margin: 8px 0; }
20
+ #feedbackArea span { font-weight: bold; color: #333; }
21
+ /* .hidden class is not strictly needed as JS controls display style directly */
22
+ /* However, if used by JS: .hidden { display: none; } */
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <h1>Live Fitness Trainer Test (WebSocket)</h1>
27
+
28
+ <div id="controls">
29
+ <label for="exerciseTypeSelect">Exercise:</label>
30
+ <select id="exerciseTypeSelect">
31
+ <option value="squat">Squat</option>
32
+ <option value="push_up">Push Up</option>
33
+ <option value="hammer_curl">Hammer Curl</option>
34
+ </select>
35
+ <button id="startButton">Start Trainer</button>
36
+ <button id="stopButton" disabled>Stop Trainer</button>
37
+ </div>
38
+
39
+ <div id="videoContainer">
40
+ <div>
41
+ <h3>Your Webcam</h3>
42
+ <video id="videoElement" width="320" height="240" autoplay playsinline></video>
43
+ </div>
44
+ <div id="feedbackArea">
45
+ <h3>Feedback & Status</h3>
46
+ <!-- These are not directly updated by websocket_trainer.js but can be kept for manual/other updates if needed -->
47
+ <p>Session ID: <span id="sessionIdDisplay">-</span></p> <!-- websocket_trainer.js doesn't update this -->
48
+ <p>Status: <span id="statusDisplay">Idle</span></p> <!-- websocket_trainer.js doesn't update this -->
49
+ <hr>
50
+
51
+ <!-- Generic UI for Squat/Push-up -->
52
+ <div class="generic-exercise-specific">
53
+ <p>Reps: <span id="repsDisplay">0</span></p>
54
+ <p>Stage: <span id="stageDisplay">-</span></p>
55
+ <p>Feedback: <span id="feedbackDisplay">-</span></p>
56
+ <p>Angle: <span id="angleDisplay">-</span></p>
57
+ </div>
58
+
59
+ <!-- Specific UI for Hammer Curl -->
60
+ <div class="hammer-curl-specific" style="display: none;"> <!-- Initially hidden by style, JS will manage -->
61
+ <p>Reps Left: <span id="repsLeftDisplay">0</span> | Reps Right: <span id="repsRightDisplay">0</span></p>
62
+ <p>Stage Left: <span id="stageLeftDisplay">-</span> | Stage Right: <span id="stageRightDisplay">-</span></p>
63
+ <p>Feedback Left: <span id="feedbackLeftDisplay">-</span></p>
64
+ <p>Feedback Right: <span id="feedbackRightDisplay">-</span></p>
65
+ <!-- The generic 'angleDisplay' is used for hammer curl angles by websocket_trainer.js -->
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ <script src="static/js/websocket_trainer.js"></script>
71
+ </body>
72
+ </html>
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/.DS_Store ADDED
Binary file (6.15 kB). View file
 
output/images/Screenshot 2024-09-08 030742.png ADDED

Git LFS Details

  • SHA256: 926c32240058e63f8e15d3624043a9a5d0fe8ded3a5e320b343f25711e6a6aa8
  • Pointer size: 132 Bytes
  • Size of remote file: 1.15 MB
output/images/Screenshot 2024-09-08 030816.png ADDED

Git LFS Details

  • SHA256: adde99a2b45c93652ff22466fafd611db1d4670615f17ebdbb3fc4e732bec601
  • Pointer size: 132 Bytes
  • Size of remote file: 1.09 MB
output/images/Screenshot 2024-09-08 030836.png ADDED

Git LFS Details

  • SHA256: c1848c95377dbb47b481205b0e9bda4a992d9e0bfaadba8bee0d241fcd62dbe5
  • Pointer size: 132 Bytes
  • Size of remote file: 2.45 MB
pose_estimation/__init__.py ADDED
File without changes
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