Spaces:
Sleeping
Sleeping
Niharmahesh
commited on
Upload 12 files
Browse files- C_pushup.mp4 +0 -0
- README.md +1 -13
- W_58.mp4 +0 -0
- W_pushup.mp4 +0 -0
- demo.mp4 +0 -0
- ltt.json +1 -0
- main.py +205 -0
- packages.txt +2 -0
- pages/pushup.py +215 -0
- pages/squat.py +214 -0
- pose_landmarks_index.png +0 -0
- requirements.txt +7 -0
C_pushup.mp4
ADDED
Binary file (207 kB). View file
|
|
README.md
CHANGED
@@ -1,13 +1 @@
|
|
1 |
-
|
2 |
-
title: Wokrout Eaasz
|
3 |
-
emoji: 🐠
|
4 |
-
colorFrom: red
|
5 |
-
colorTo: yellow
|
6 |
-
sdk: streamlit
|
7 |
-
sdk_version: 1.40.1
|
8 |
-
app_file: app.py
|
9 |
-
pinned: false
|
10 |
-
short_description: 'corrects your pushup form and squat form '
|
11 |
-
---
|
12 |
-
|
13 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
1 |
+
# squat_easz
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
W_58.mp4
ADDED
Binary file (69.8 kB). View file
|
|
W_pushup.mp4
ADDED
Binary file (213 kB). View file
|
|
demo.mp4
ADDED
Binary file (562 kB). View file
|
|
ltt.json
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
{"v":"5.6.1","fr":29.9700012207031,"ip":0,"op":154.000006272549,"w":1000,"h":1000,"nm":"gym 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Layer 5 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":27,"s":[517,497.838,0],"to":[10,0,0],"ti":[-10,0,0]},{"i":{"x":0,"y":0},"o":{"x":0.167,"y":0.167},"t":56,"s":[577,497.838,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":70,"s":[577,497.838,0],"to":[20,0,0],"ti":[-20,0,0]},{"t":94.0000038286985,"s":[697,497.838,0]}],"ix":2},"a":{"a":0,"k":[703,497.838,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[18.777,0],[0,0],[0,18.777],[0,0],[-18.777,0],[0,-18.778],[0,0]],"o":[[0,0],[-18.777,0],[0,0],[0,-18.778],[18.777,0],[0,0],[0,18.777]],"v":[[0,136],[0,136],[-34,102],[-34,-102],[0,-136],[34,-102],[34,102]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[703,497.838],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":27.0000010997325,"op":178.000007250089,"st":-3.00000012219251,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Layer 4 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":27,"s":[454,497.838,0],"to":[-10,0,0],"ti":[10,0,0]},{"i":{"x":0,"y":0},"o":{"x":0.167,"y":0.167},"t":56,"s":[394,497.838,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":70,"s":[394,497.838,0],"to":[-20,0,0],"ti":[20,0,0]},{"t":94.0000038286985,"s":[274,497.838,0]}],"ix":2},"a":{"a":0,"k":[268,497.838,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[18.778,0],[0,0],[0,18.777],[0,0],[-18.778,0],[0,-18.778],[0,0]],"o":[[0,0],[-18.778,0],[0,0],[0,-18.778],[18.778,0],[0,0],[0,18.777]],"v":[[0,136],[0,136],[-34,102],[-34,-102],[0,-136],[34,-102],[34,102]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[268,497.838],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":27.0000010997325,"op":178.000007250089,"st":-3.00000012219251,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Layer 6 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.165,"y":1},"o":{"x":0.014,"y":0},"t":9,"s":[487.941,497.838,0],"to":[-6.589,0,0],"ti":[13.944,0,0]},{"i":{"x":0.011,"y":1},"o":{"x":0.167,"y":0},"t":27,"s":[455.935,497.838,0],"to":[-42.489,0,0],"ti":[0,0,0]},{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":70,"s":[455.935,497.838,0],"to":[0,0,0],"ti":[0,0,0]},{"t":94.0000038286985,"s":[335.935,497.838,0]}],"ix":2},"a":{"a":0,"k":[327.941,497.838,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[20.955,0],[0,0],[0,20.954],[0,0],[-20.955,0],[0,-20.955],[0,0]],"o":[[0,0],[-20.955,0],[0,0],[0,-20.955],[20.955,0],[0,0],[0,20.954]],"v":[[0,187.176],[0,187.176],[-37.941,149.235],[-37.941,-149.234],[0,-187.176],[37.941,-149.234],[37.941,149.235]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[327.941,497.838],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":9.00000036657752,"op":160.000006516934,"st":9.00000036657752,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Layer 3 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.165,"y":1},"o":{"x":0.014,"y":0},"t":9,"s":[486.941,497.838,0],"to":[6.507,0,0],"ti":[-13.771,0,0]},{"i":{"x":0.011,"y":1},"o":{"x":0.167,"y":0},"t":27,"s":[518.551,497.838,0],"to":[41.958,0,0],"ti":[0,0,0]},{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":70,"s":[518.551,497.838,0],"to":[0,0,0],"ti":[0,0,0]},{"t":94.0000038286985,"s":[638.551,497.838,0]}],"ix":2},"a":{"a":0,"k":[644.941,497.838,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[0,0,100]},{"t":8.00000032584668,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[20.954,0],[0,0],[0,20.954],[0,0],[-20.954,0],[0,-20.955],[0,0]],"o":[[0,0],[-20.954,0],[0,0],[0,-20.955],[20.954,0],[0,0],[0,20.954]],"v":[[0,187.176],[0,187.176],[-37.941,149.235],[-37.941,-149.234],[0,-187.176],[37.941,-149.234],[37.941,149.235]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[644.941,497.838],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":160.000006516934,"st":-6.00000024438501,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Layer 2 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0,"y":0},"t":89,"s":[-354.944,492.522,0],"to":[140,0,0],"ti":[-140,0,0]},{"t":106.000004317469,"s":[485.056,492.522,0]}],"ix":2},"a":{"a":0,"k":[485.056,492.522,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[18.225,0],[0,0],[0,18.226],[-18.226,0],[0,0],[0,-18.225]],"o":[[0,0],[-18.226,0],[0,-18.225],[0,0],[18.225,0],[0,18.226]],"v":[[296.901,33],[-296.9,33],[-329.9,0],[-296.9,-33],[296.901,-33],[329.9,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[485.056,492.522],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":9.00000036657752,"op":160.000006516934,"st":9.00000036657752,"bm":0},{"ddd":0,"ind":6,"ty":1,"nm":"White Solid 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[500,500,0],"ix":2},"a":{"a":0,"k":[500,500,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"sw":1000,"sh":1000,"sc":"#ffffff","ip":0,"op":154.000006272549,"st":0,"bm":0}],"markers":[]}
|
main.py
ADDED
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import json
|
3 |
+
from streamlit_lottie import st_lottie
|
4 |
+
|
5 |
+
# Load Lottie animation from a JSON file
|
6 |
+
def load_lottiefile(filepath: str):
|
7 |
+
with open(filepath, "r") as f:
|
8 |
+
return json.load(f)
|
9 |
+
st.set_page_config(layout="wide")
|
10 |
+
|
11 |
+
st.markdown("<h1 style='text-align: center;'>Workout Easz</h1>", unsafe_allow_html=True)
|
12 |
+
lottie_animation = load_lottiefile("ltt.json")
|
13 |
+
st_lottie(lottie_animation, height=300, key="header_animation")
|
14 |
+
# Disclaimer
|
15 |
+
st.markdown("""
|
16 |
+
<div style="text-align: center; color: #FF0000;">
|
17 |
+
<strong>Disclaimer:</strong>
|
18 |
+
The performance of pose detection depends on the model's accuracy. As of now, our application uses a model with approximately 50% pose detection accuracy. Results may vary based on the quality of the video input and the model's limitations.
|
19 |
+
</div>
|
20 |
+
""", unsafe_allow_html=True)
|
21 |
+
# Introduction and Key Points
|
22 |
+
with st.expander("Introduction to MediaPipe"):
|
23 |
+
st.header("Introduction to MediaPipe")
|
24 |
+
st.write("""
|
25 |
+
MediaPipe is an open-source framework developed by Google for building multimodal machine learning pipelines. Its Pose solution is designed for high-fidelity body pose tracking, detecting 33 key body landmarks in real-time[1][2].
|
26 |
+
""")
|
27 |
+
|
28 |
+
with st.expander("Extraction of 33 Key Points"):
|
29 |
+
st.header("Extraction of 33 Key Points")
|
30 |
+
st.write("""
|
31 |
+
MediaPipe Pose detects 33 key body landmarks, each with x, y, z coordinates, and a visibility score. These landmarks represent various parts of the body, such as the nose, eyes, ears, shoulders, elbows, wrists, hips, knees, ankles, and feet[1][2].
|
32 |
+
""")
|
33 |
+
st.image("pose_landmarks_index.png", caption="MediaPipe Pose Landmarks", width=400)
|
34 |
+
|
35 |
+
# Squat Analysis
|
36 |
+
squat_expander = st.expander("Squat Form Analysis")
|
37 |
+
with squat_expander:
|
38 |
+
st.header("Squat Form Analysis")
|
39 |
+
|
40 |
+
# Key Landmarks for Squat Analysis
|
41 |
+
st.subheader("1. Key Landmarks for Squat Analysis")
|
42 |
+
st.write("""
|
43 |
+
For squat analysis, we focus on the following key landmarks:
|
44 |
+
- Shoulder (left and right)
|
45 |
+
- Hip (left and right)
|
46 |
+
- Knee (left and right)
|
47 |
+
- Ankle (left and right)
|
48 |
+
|
49 |
+
These landmarks are essential for calculating the joint angles critical for analyzing squat form[3].
|
50 |
+
""")
|
51 |
+
|
52 |
+
# Calculation of Joint Angles for Squat
|
53 |
+
st.subheader("2. Calculation of Joint Angles for Squat")
|
54 |
+
|
55 |
+
# Knee Angle
|
56 |
+
st.write("**a. Knee Angle**")
|
57 |
+
st.write("""
|
58 |
+
The knee angle is formed by the hip, knee, and ankle. It is calculated using the arctangent function:
|
59 |
+
""")
|
60 |
+
st.latex(r"\theta_{\text{knee}} = \left| \frac{180}{\pi} \times \left( \arctan2(y_{\text{ankle}} - y_{\text{knee}}, x_{\text{ankle}} - x_{\text{knee}}) - \arctan2(y_{\text{hip}} - y_{\text{knee}}, x_{\text{hip}} - x_{\text{knee}}) \right) \right|")
|
61 |
+
|
62 |
+
# Hip Angle
|
63 |
+
st.write("**b. Hip Angle**")
|
64 |
+
st.write("""
|
65 |
+
The hip angle is formed by the shoulder, hip, and knee. It is calculated similarly:
|
66 |
+
""")
|
67 |
+
st.latex(r"\theta_{\text{hip}} = \left| \frac{180}{\pi} \times \left( \arctan2(y_{\text{knee}} - y_{\text{hip}}, x_{\text{knee}} - x_{\text{hip}}) - \arctan2(y_{\text{shoulder}} - y_{\text{hip}}, x_{\text{shoulder}} - x_{\text{hip}}) \right) \right|")
|
68 |
+
|
69 |
+
# Back Angle
|
70 |
+
st.write("**c. Back Angle**")
|
71 |
+
st.write("""
|
72 |
+
The back angle is formed by the shoulder, hip, and ankle:
|
73 |
+
""")
|
74 |
+
st.latex(r"\theta_{\text{back}} = \left| \frac{180}{\pi} \times \left( \arctan2(y_{\text{ankle}} - y_{\text{hip}}, x_{\text{ankle}} - x_{\text{hip}}) - \arctan2(y_{\text{shoulder}} - y_{\text{hip}}, x_{\text{shoulder}} - x_{\text{hip}}) \right) \right|")
|
75 |
+
|
76 |
+
# Form Issue Detection for Squat
|
77 |
+
st.subheader("3. Form Issue Detection for Squat")
|
78 |
+
|
79 |
+
st.write("**Ideal Angle Ranges:**")
|
80 |
+
st.write("""
|
81 |
+
- Knee Angle: 90 to 110 degrees
|
82 |
+
- Hip Angle: 80 to 100 degrees
|
83 |
+
- Back Angle: 70 to 90 degrees
|
84 |
+
""")
|
85 |
+
|
86 |
+
st.write("**Detection of Deviations:**")
|
87 |
+
st.write("""
|
88 |
+
- Knees Bending Too Much: Knee angle < 90 degrees
|
89 |
+
- Hips Bending Too Much: Hip angle < 80 degrees
|
90 |
+
- Back Leaning Too Much: Back angle < 70 degrees
|
91 |
+
""")
|
92 |
+
|
93 |
+
# Providing Suggestions for Squat
|
94 |
+
st.subheader("4. Providing Suggestions for Squat")
|
95 |
+
st.write("""
|
96 |
+
Based on the detected form issues, specific suggestions are provided:
|
97 |
+
|
98 |
+
1. Knees Bending Too Much:
|
99 |
+
- Suggestion: "Watch your knee bend. Ensure your knees do not bend excessively and maintain proper alignment with your toes."
|
100 |
+
|
101 |
+
2. Hips Bending Too Much:
|
102 |
+
- Suggestion: "Keep your hips higher. Avoid excessive hip bending by maintaining a more upright posture."
|
103 |
+
|
104 |
+
3. Back Leaning Too Much:
|
105 |
+
- Suggestion: "Maintain a straighter back. Focus on keeping your back straight and avoid leaning forward excessively."
|
106 |
+
""")
|
107 |
+
|
108 |
+
# Push-up Analysis
|
109 |
+
pushup_expander = st.expander("Push-up Form Analysis")
|
110 |
+
with pushup_expander:
|
111 |
+
st.header("Push-up Form Analysis")
|
112 |
+
|
113 |
+
# Key Landmarks for Push-up Analysis
|
114 |
+
st.subheader("1. Key Landmarks for Push-up Analysis")
|
115 |
+
st.write("""
|
116 |
+
For push-up analysis, we focus on the following key landmarks:
|
117 |
+
- Shoulder (left and right)
|
118 |
+
- Elbow (left and right)
|
119 |
+
- Wrist (left and right)
|
120 |
+
- Hip (left and right)
|
121 |
+
- Ankle (left and right)
|
122 |
+
|
123 |
+
These landmarks are crucial for calculating the joint angles and body alignment in push-ups[3].
|
124 |
+
""")
|
125 |
+
|
126 |
+
# Calculation of Joint Angles for Push-up
|
127 |
+
st.subheader("2. Calculation of Joint Angles for Push-up")
|
128 |
+
|
129 |
+
# Elbow Angle
|
130 |
+
st.write("**a. Elbow Angle**")
|
131 |
+
st.write("""
|
132 |
+
The elbow angle is formed by the shoulder, elbow, and wrist. It is calculated using:
|
133 |
+
""")
|
134 |
+
st.latex(r"\theta_{\text{elbow}} = \left| \frac{180}{\pi} \times \left( \arctan2(y_{\text{wrist}} - y_{\text{elbow}}, x_{\text{wrist}} - x_{\text{elbow}}) - \arctan2(y_{\text{shoulder}} - y_{\text{elbow}}, x_{\text{shoulder}} - x_{\text{elbow}}) \right) \right|")
|
135 |
+
|
136 |
+
# Body Alignment Angle
|
137 |
+
st.write("**b. Body Alignment Angle**")
|
138 |
+
st.write("""
|
139 |
+
The body alignment angle is formed by the shoulder, hip, and ankle. It is calculated as:
|
140 |
+
""")
|
141 |
+
st.latex(r"\theta_{\text{alignment}} = \left| \frac{180}{\pi} \times \left( \arctan2(y_{\text{ankle}} - y_{\text{hip}}, x_{\text{ankle}} - x_{\text{hip}}) - \arctan2(y_{\text{shoulder}} - y_{\text{hip}}, x_{\text{shoulder}} - x_{\text{hip}}) \right) \right|")
|
142 |
+
|
143 |
+
# Form Issue Detection for Push-up
|
144 |
+
st.subheader("3. Form Issue Detection for Push-up")
|
145 |
+
|
146 |
+
st.write("**Ideal Angle Ranges:**")
|
147 |
+
st.write("""
|
148 |
+
- Elbow Angle: 90 to 110 degrees (at the bottom of the push-up)
|
149 |
+
- Body Alignment Angle: 170 to 180 degrees (straight body)
|
150 |
+
""")
|
151 |
+
|
152 |
+
st.write("**Detection of Deviations:**")
|
153 |
+
st.write("""
|
154 |
+
- Incomplete Push-up: Elbow angle > 110 degrees at the bottom
|
155 |
+
- Elbow Flaring: Elbow angle < 90 degrees
|
156 |
+
- Sagging Hips: Body alignment angle < 170 degrees
|
157 |
+
""")
|
158 |
+
|
159 |
+
# Providing Suggestions for Push-up
|
160 |
+
st.subheader("4. Providing Suggestions for Push-up")
|
161 |
+
st.write("""
|
162 |
+
Based on the detected form issues, specific suggestions are provided:
|
163 |
+
|
164 |
+
1. Incomplete Push-up:
|
165 |
+
- Suggestion: "Lower your body further. Aim to bring your chest closer to the ground for a full range of motion."
|
166 |
+
|
167 |
+
2. Elbow Flaring:
|
168 |
+
- Suggestion: "Keep your elbows closer to your body. Tuck them in to engage your chest and triceps more effectively."
|
169 |
+
|
170 |
+
3. Sagging Hips:
|
171 |
+
- Suggestion: "Maintain a straight body line. Engage your core to keep your hips aligned with your shoulders and ankles."
|
172 |
+
""")
|
173 |
+
|
174 |
+
# Conclusion
|
175 |
+
with st.expander("Conclusion",expanded=False):
|
176 |
+
st.write("""
|
177 |
+
By leveraging MediaPipe's pose estimation capabilities, this application can effectively analyze both squat and push-up forms. It calculates key joint angles and body alignments, compares them to ideal ranges, and provides specific suggestions for improvement. This approach enables users to receive real-time feedback on their exercise technique, helping them to perform exercises correctly and reduce the risk of injury[1][2][3].
|
178 |
+
""")
|
179 |
+
# Version Updates
|
180 |
+
with st.expander("Version Updates", expanded=False):
|
181 |
+
st.header("Version Updates")
|
182 |
+
|
183 |
+
st.subheader("Current Version")
|
184 |
+
st.write("""
|
185 |
+
**Current Update:** As of today, our application now includes both squat and push-up form analysis.
|
186 |
+
""")
|
187 |
+
|
188 |
+
st.subheader("Previous Versions")
|
189 |
+
st.write("""
|
190 |
+
**Initial Release:** Squat workout analysis, released on July 19th.
|
191 |
+
""")
|
192 |
+
|
193 |
+
st.write("""
|
194 |
+
Stay tuned for more updates as we continue to enhance our application's features and performance.
|
195 |
+
""")
|
196 |
+
# References
|
197 |
+
with st.expander("References"):
|
198 |
+
st.header("References")
|
199 |
+
st.write("""
|
200 |
+
[1] [MediaPipe Pose Documentation](https://github.com/google-ai-edge/mediapipe/blob/master/docs/solutions/pose.md)
|
201 |
+
[2] [Google AI Edge - MediaPipe Pose Landmarker](https://ai.google.dev/edge/mediapipe/solutions/vision/pose_landmarker)
|
202 |
+
[3] [Real-time Human Pose Estimation using MediaPipe](https://sigmoidal.ai/en/real-time-human-pose-estimation-using-mediapipe/)
|
203 |
+
[4] [Kaggle Dataset for Push-Up Analysis](https://www.kaggle.com/datasets/mohamadashrafsalama/pushup)
|
204 |
+
""")
|
205 |
+
|
packages.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
libgl1-mesa-glx
|
2 |
+
libglib2.0-0
|
pages/pushup.py
ADDED
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import cv2
|
3 |
+
import mediapipe as mp
|
4 |
+
import numpy as np
|
5 |
+
import time
|
6 |
+
import json
|
7 |
+
|
8 |
+
# Cache the MediaPipe Pose model
|
9 |
+
@st.cache_resource
|
10 |
+
def load_pose_model():
|
11 |
+
mp_pose = mp.solutions.pose
|
12 |
+
pose = mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5)
|
13 |
+
return mp_pose, pose
|
14 |
+
|
15 |
+
mp_pose, pose = load_pose_model()
|
16 |
+
mp_drawing = mp.solutions.drawing_utils
|
17 |
+
|
18 |
+
# Function to calculate angle
|
19 |
+
def calculate_angle(a, b, c):
|
20 |
+
a = np.array(a) # First point
|
21 |
+
b = np.array(b) # Mid point
|
22 |
+
c = np.array(c) # End point
|
23 |
+
radians = np.arctan2(c[1] - b[1], c[0] - b[0]) - np.arctan2(a[1] - b[1], a[0] - b[0])
|
24 |
+
angle = np.abs(radians * 180.0 / np.pi)
|
25 |
+
if angle > 180.0:
|
26 |
+
angle = 360 - angle
|
27 |
+
return angle
|
28 |
+
|
29 |
+
# Function to calculate body angle
|
30 |
+
def calculate_body_angle(shoulder, hip, ankle):
|
31 |
+
shoulder = np.array(shoulder)
|
32 |
+
hip = np.array(hip)
|
33 |
+
ankle = np.array(ankle)
|
34 |
+
radians = np.arctan2(ankle[1] - hip[1], ankle[0] - hip[0]) - np.arctan2(shoulder[1] - hip[1], shoulder[0] - hip[0])
|
35 |
+
angle = np.abs(radians * 180.0 / np.pi)
|
36 |
+
if angle > 180.0:
|
37 |
+
angle = 360 - angle
|
38 |
+
return angle
|
39 |
+
|
40 |
+
# Function to calculate rep score
|
41 |
+
def calculate_rep_score(elbow_angles, body_angles):
|
42 |
+
ideal_elbow_range = (75, 90)
|
43 |
+
ideal_body_range = (0, 10)
|
44 |
+
elbow_score = sum(1 for angle in elbow_angles if ideal_elbow_range[0] <= angle <= ideal_elbow_range[1]) / len(elbow_angles) if elbow_angles else 0
|
45 |
+
body_score = sum(1 for angle in body_angles if ideal_body_range[0] <= angle <= ideal_body_range[1]) / len(body_angles) if body_angles else 0
|
46 |
+
return (elbow_score + body_score) / 2
|
47 |
+
|
48 |
+
# Function to generate workout report
|
49 |
+
def generate_workout_report(rep_scores, form_issues, analysis_time):
|
50 |
+
overall_efficiency = sum(rep_scores) / len(rep_scores) if rep_scores else 0
|
51 |
+
total_reps = len(rep_scores)
|
52 |
+
elbows_not_bending_enough = form_issues['elbows_not_bending_enough']
|
53 |
+
body_not_straight = form_issues['body_not_straight']
|
54 |
+
|
55 |
+
report = f"""
|
56 |
+
**Workout Report:**
|
57 |
+
-----------------
|
58 |
+
- **Total Push-ups:** {total_reps}
|
59 |
+
- **Overall Workout Efficiency:** {overall_efficiency * 100:.2f}%
|
60 |
+
- **Analysis Time:** {analysis_time:.2f} seconds
|
61 |
+
|
62 |
+
**Form Issues:**
|
63 |
+
- Elbows not bending enough: {elbows_not_bending_enough} reps ({(elbows_not_bending_enough / total_reps) * 100:.2f}% of reps)
|
64 |
+
- Body not straight: {body_not_straight} reps ({(body_not_straight / total_reps) * 100:.2f}% of reps)
|
65 |
+
"""
|
66 |
+
return report
|
67 |
+
|
68 |
+
# Load Lottie animation from a JSON file
|
69 |
+
def load_lottiefile(filepath: str):
|
70 |
+
with open(filepath, "r") as f:
|
71 |
+
return json.load(f)
|
72 |
+
|
73 |
+
# Streamlit app
|
74 |
+
st.markdown("<h1 style='text-align: center;'>Push-Up Form Analysis</h1>", unsafe_allow_html=True)
|
75 |
+
|
76 |
+
# Center the "Try Demo" button
|
77 |
+
col1, col2, col3 = st.columns([1, 2, 1])
|
78 |
+
with col2:
|
79 |
+
demo_button = st.button("Try Demo")
|
80 |
+
|
81 |
+
# Path to the demo video
|
82 |
+
demo_video_path = "W_58.mp4"
|
83 |
+
|
84 |
+
# Video selection logic
|
85 |
+
video_path = None
|
86 |
+
if demo_button:
|
87 |
+
video_path = demo_video_path
|
88 |
+
st.success("Demo video loaded successfully!")
|
89 |
+
|
90 |
+
st.write("Or upload your own video:")
|
91 |
+
# File uploader for user's video
|
92 |
+
uploaded_file = st.file_uploader("Choose a video file", type=["mp4", "mov", "avi"])
|
93 |
+
if uploaded_file is not None:
|
94 |
+
with open("temp_video.mp4", "wb") as f:
|
95 |
+
f.write(uploaded_file.getvalue())
|
96 |
+
video_path = "temp_video.mp4"
|
97 |
+
st.success("Your video uploaded successfully!")
|
98 |
+
|
99 |
+
if video_path:
|
100 |
+
cap = cv2.VideoCapture(video_path)
|
101 |
+
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
102 |
+
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
103 |
+
fps = cap.get(cv2.CAP_PROP_FPS)
|
104 |
+
st.write(f"Frames per second: {fps}")
|
105 |
+
|
106 |
+
# Create placeholders for the three video outputs with titles
|
107 |
+
col1, col2, col3 = st.columns(3)
|
108 |
+
with col1:
|
109 |
+
st.subheader("Original Video")
|
110 |
+
original_video = st.empty()
|
111 |
+
with col2:
|
112 |
+
st.subheader("Pose Points")
|
113 |
+
points_video = st.empty()
|
114 |
+
with col3:
|
115 |
+
st.subheader("Form Guide")
|
116 |
+
guide_video = st.empty()
|
117 |
+
|
118 |
+
feedback_placeholder = st.empty()
|
119 |
+
|
120 |
+
rep_scores = []
|
121 |
+
current_rep_angles = {'elbow': [], 'body': []}
|
122 |
+
form_issues = {
|
123 |
+
"elbows_not_bending_enough": 0,
|
124 |
+
"body_not_straight": 0
|
125 |
+
}
|
126 |
+
stage = "UP"
|
127 |
+
pushup_count = 0
|
128 |
+
start_time = time.time()
|
129 |
+
|
130 |
+
# Initialize form issues for the current rep
|
131 |
+
current_rep_issues = {
|
132 |
+
"elbows_not_bending_enough": False,
|
133 |
+
"body_not_straight": False
|
134 |
+
}
|
135 |
+
|
136 |
+
try:
|
137 |
+
while cap.isOpened():
|
138 |
+
ret, frame = cap.read()
|
139 |
+
if not ret:
|
140 |
+
break
|
141 |
+
|
142 |
+
image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
143 |
+
image.flags.writeable = False
|
144 |
+
results = pose.process(image)
|
145 |
+
image.flags.writeable = True
|
146 |
+
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
147 |
+
|
148 |
+
if results.pose_landmarks:
|
149 |
+
try:
|
150 |
+
landmarks = results.pose_landmarks.landmark
|
151 |
+
shoulder = [landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x * width,
|
152 |
+
landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y * height]
|
153 |
+
elbow = [landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x * width,
|
154 |
+
landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].y * height]
|
155 |
+
wrist = [landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x * width,
|
156 |
+
landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y * height]
|
157 |
+
hip = [landmarks[mp_pose.PoseLandmark.LEFT_HIP.value].x * width,
|
158 |
+
landmarks[mp_pose.PoseLandmark.LEFT_HIP.value].y * height]
|
159 |
+
ankle = [landmarks[mp_pose.PoseLandmark.LEFT_ANKLE.value].x * width,
|
160 |
+
landmarks[mp_pose.PoseLandmark.LEFT_ANKLE.value].y * height]
|
161 |
+
|
162 |
+
angle_elbow = calculate_angle(shoulder, elbow, wrist)
|
163 |
+
angle_body = calculate_body_angle(shoulder, hip, ankle)
|
164 |
+
|
165 |
+
current_rep_angles['elbow'].append(angle_elbow)
|
166 |
+
current_rep_angles['body'].append(angle_body)
|
167 |
+
|
168 |
+
points_image = np.zeros((height, width, 3), dtype=np.uint8)
|
169 |
+
guide_image = np.zeros((height, width, 3), dtype=np.uint8)
|
170 |
+
mp_drawing.draw_landmarks(points_image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS)
|
171 |
+
mp_drawing.draw_landmarks(guide_image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS)
|
172 |
+
|
173 |
+
if angle_elbow < 75:
|
174 |
+
current_rep_issues["elbows_not_bending_enough"] = True
|
175 |
+
cv2.line(guide_image, tuple(np.multiply(elbow, [1, 1]).astype(int)),
|
176 |
+
tuple(np.multiply(wrist, [1, 1]).astype(int)), (0, 255, 255), 5) # Yellow for elbow issues
|
177 |
+
if angle_body > 10:
|
178 |
+
current_rep_issues["body_not_straight"] = True
|
179 |
+
cv2.line(guide_image, tuple(np.multiply(shoulder, [1, 1]).astype(int)),
|
180 |
+
tuple(np.multiply(hip, [1, 1]).astype(int)), (0, 0, 255), 5) # Red for body issues
|
181 |
+
|
182 |
+
if angle_elbow > 90 and stage != "UP":
|
183 |
+
stage = "UP"
|
184 |
+
# Count issues for the completed rep
|
185 |
+
for issue, occurred in current_rep_issues.items():
|
186 |
+
if occurred:
|
187 |
+
form_issues[issue] += 1
|
188 |
+
# Reset current rep issues
|
189 |
+
current_rep_issues = {k: False for k in current_rep_issues}
|
190 |
+
|
191 |
+
if angle_elbow < 90 and stage == "UP":
|
192 |
+
stage = "DOWN"
|
193 |
+
pushup_count += 1
|
194 |
+
rep_score = calculate_rep_score(current_rep_angles['elbow'], current_rep_angles['body'])
|
195 |
+
rep_scores.append(rep_score)
|
196 |
+
current_rep_angles = {'elbow': [], 'body': []}
|
197 |
+
|
198 |
+
# Display the videos
|
199 |
+
original_video.image(image, channels="BGR")
|
200 |
+
points_video.image(points_image, channels="BGR")
|
201 |
+
guide_video.image(guide_image, channels="BGR")
|
202 |
+
except AttributeError as e:
|
203 |
+
st.error(f"Error processing frame: {e}")
|
204 |
+
else:
|
205 |
+
st.warning("No pose landmarks detected in this frame.")
|
206 |
+
except Exception as e:
|
207 |
+
st.error(f"Error occurred: {e}")
|
208 |
+
finally:
|
209 |
+
cap.release()
|
210 |
+
|
211 |
+
analysis_time = time.time() - start_time
|
212 |
+
report = generate_workout_report(rep_scores, form_issues, analysis_time)
|
213 |
+
feedback_placeholder.markdown(report)
|
214 |
+
|
215 |
+
#st.balloons() # Add a fun animation when the analysis is done
|
pages/squat.py
ADDED
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import cv2
|
3 |
+
import mediapipe as mp
|
4 |
+
import numpy as np
|
5 |
+
import time
|
6 |
+
import json
|
7 |
+
|
8 |
+
@st.cache_resource
|
9 |
+
def load_pose_model():
|
10 |
+
mp_pose = mp.solutions.pose
|
11 |
+
pose = mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5)
|
12 |
+
return mp_pose, pose
|
13 |
+
|
14 |
+
mp_pose, pose = load_pose_model()
|
15 |
+
mp_drawing = mp.solutions.drawing_utils
|
16 |
+
|
17 |
+
def calculate_angle(a, b, c):
|
18 |
+
a = np.array(a) # First
|
19 |
+
b = np.array(b) # Mid
|
20 |
+
c = np.array(c) # End
|
21 |
+
radians = np.arctan2(c[1] - b[1], c[0] - b[0]) - np.arctan2(a[1] - b[1], a[0] - b[0])
|
22 |
+
angle = np.abs(radians * 180.0 / np.pi)
|
23 |
+
if angle > 180.0:
|
24 |
+
angle = 360 - angle
|
25 |
+
return angle
|
26 |
+
|
27 |
+
def calculate_rep_score(knee_angles, hip_angles, back_angles):
|
28 |
+
ideal_knee_range = (90, 110)
|
29 |
+
ideal_hip_range = (80, 100)
|
30 |
+
ideal_back_range = (70, 90)
|
31 |
+
knee_score = sum(1 for angle in knee_angles if ideal_knee_range[0] <= angle <= ideal_knee_range[1]) / len(knee_angles) if knee_angles else 0
|
32 |
+
hip_score = sum(1 for angle in hip_angles if ideal_hip_range[0] <= angle <= ideal_hip_range[1]) / len(hip_angles) if hip_angles else 0
|
33 |
+
back_score = sum(1 for angle in back_angles if ideal_back_range[0] <= angle <= ideal_back_range[1]) / len(back_angles) if back_angles else 0
|
34 |
+
return (knee_score + hip_score + back_score) / 3
|
35 |
+
|
36 |
+
def generate_workout_report(rep_scores, form_issues, analysis_time):
|
37 |
+
if rep_scores:
|
38 |
+
overall_efficiency = sum(rep_scores) / len(rep_scores)
|
39 |
+
else:
|
40 |
+
overall_efficiency = 0
|
41 |
+
|
42 |
+
total_reps = len(rep_scores)
|
43 |
+
|
44 |
+
if total_reps > 0:
|
45 |
+
knee_percentage = form_issues['knees_bending_too_much'] / total_reps * 100
|
46 |
+
hip_percentage = form_issues['hips_bending_too_much'] / total_reps * 100
|
47 |
+
back_percentage = form_issues['back_leaning_too_much'] / total_reps * 100
|
48 |
+
else:
|
49 |
+
knee_percentage = 0
|
50 |
+
hip_percentage = 0
|
51 |
+
back_percentage = 0
|
52 |
+
|
53 |
+
report = f"""
|
54 |
+
Workout Report:
|
55 |
+
---------------
|
56 |
+
Total Squats: {total_reps}
|
57 |
+
Overall Workout Efficiency: {overall_efficiency * 100:.2f}%
|
58 |
+
Analysis Time: {analysis_time:.2f} seconds
|
59 |
+
Form Issues:
|
60 |
+
- Knees bending too much: {form_issues['knees_bending_too_much']} reps ({knee_percentage:.2f}% of reps)
|
61 |
+
- Hips bending too much: {form_issues['hips_bending_too_much']} reps ({hip_percentage:.2f}% of reps)
|
62 |
+
- Back leaning too much: {form_issues['back_leaning_too_much']} reps ({back_percentage:.2f}% of reps)
|
63 |
+
"""
|
64 |
+
return report
|
65 |
+
|
66 |
+
def load_lottiefile(filepath: str):
|
67 |
+
with open(filepath, "r") as f:
|
68 |
+
return json.load(f)
|
69 |
+
|
70 |
+
st.markdown("<h1 style='text-align: center;'>Squat Form Analysis</h1>", unsafe_allow_html=True)
|
71 |
+
|
72 |
+
col1, col2, col3 = st.columns([1,2,1])
|
73 |
+
with col2:
|
74 |
+
demo_button = st.button("Try Demo")
|
75 |
+
|
76 |
+
demo_video_path = "demo.mp4"
|
77 |
+
|
78 |
+
video_path = None
|
79 |
+
if demo_button:
|
80 |
+
video_path = demo_video_path
|
81 |
+
st.success("Demo video loaded successfully!")
|
82 |
+
|
83 |
+
st.write("Or upload your own video:")
|
84 |
+
uploaded_file = st.file_uploader("Choose a video file", type=["mp4", "mov", "avi"])
|
85 |
+
if uploaded_file is not None:
|
86 |
+
with open("temp_video.mp4", "wb") as f:
|
87 |
+
f.write(uploaded_file.getvalue())
|
88 |
+
video_path = "temp_video.mp4"
|
89 |
+
st.success("Your video uploaded successfully!")
|
90 |
+
|
91 |
+
if video_path:
|
92 |
+
cap = cv2.VideoCapture(video_path)
|
93 |
+
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
94 |
+
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
95 |
+
fps = cap.get(cv2.CAP_PROP_FPS)
|
96 |
+
st.write(f"Frames per second: {fps}")
|
97 |
+
|
98 |
+
col1, col2, col3 = st.columns(3)
|
99 |
+
with col1:
|
100 |
+
st.subheader("Original Video")
|
101 |
+
original_video = st.empty()
|
102 |
+
with col2:
|
103 |
+
st.subheader("Pose Points")
|
104 |
+
points_video = st.empty()
|
105 |
+
with col3:
|
106 |
+
st.subheader("Form Guide")
|
107 |
+
guide_video = st.empty()
|
108 |
+
|
109 |
+
squat_count_placeholder = st.empty()
|
110 |
+
feedback_placeholder = st.empty()
|
111 |
+
|
112 |
+
rep_scores = []
|
113 |
+
current_rep_angles = {'knee': [], 'hip': [], 'back': []}
|
114 |
+
form_issues = {
|
115 |
+
"knees_bending_too_much": 0,
|
116 |
+
"hips_bending_too_much": 0,
|
117 |
+
"back_leaning_too_much": 0
|
118 |
+
}
|
119 |
+
stage = None
|
120 |
+
squat_count = 0
|
121 |
+
start_time = time.time()
|
122 |
+
|
123 |
+
current_rep_issues = {
|
124 |
+
"knees_bending_too_much": False,
|
125 |
+
"hips_bending_too_much": False,
|
126 |
+
"back_leaning_too_much": False
|
127 |
+
}
|
128 |
+
|
129 |
+
while cap.isOpened():
|
130 |
+
ret, frame = cap.read()
|
131 |
+
if not ret:
|
132 |
+
break
|
133 |
+
|
134 |
+
image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
135 |
+
image.flags.writeable = False
|
136 |
+
results = pose.process(image)
|
137 |
+
image.flags.writeable = True
|
138 |
+
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
139 |
+
|
140 |
+
if results.pose_landmarks is not None:
|
141 |
+
landmarks = results.pose_landmarks.landmark
|
142 |
+
shoulder = [landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x * width,
|
143 |
+
landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y * height]
|
144 |
+
hip = [landmarks[mp_pose.PoseLandmark.LEFT_HIP.value].x * width,
|
145 |
+
landmarks[mp_pose.PoseLandmark.LEFT_HIP.value].y * height]
|
146 |
+
knee = [landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value].x * width,
|
147 |
+
landmarks[mp_pose.PoseLandmark.LEFT_KNEE.value].y * height]
|
148 |
+
ankle = [landmarks[mp_pose.PoseLandmark.LEFT_ANKLE.value].x * width,
|
149 |
+
landmarks[mp_pose.PoseLandmark.LEFT_ANKLE.value].y * height]
|
150 |
+
|
151 |
+
angle_knee = calculate_angle(hip, knee, ankle)
|
152 |
+
angle_hip = calculate_angle(shoulder, hip, knee)
|
153 |
+
angle_back = calculate_angle(shoulder, hip, ankle)
|
154 |
+
|
155 |
+
current_rep_angles['knee'].append(angle_knee)
|
156 |
+
current_rep_angles['hip'].append(angle_hip)
|
157 |
+
current_rep_angles['back'].append(angle_back)
|
158 |
+
|
159 |
+
points_image = np.zeros((height, width, 3), dtype=np.uint8)
|
160 |
+
guide_image = np.zeros((height, width, 3), dtype=np.uint8)
|
161 |
+
mp_drawing.draw_landmarks(points_image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS)
|
162 |
+
|
163 |
+
# Drawing on guide image with smaller markers
|
164 |
+
mp_drawing.draw_landmarks(
|
165 |
+
guide_image,
|
166 |
+
results.pose_landmarks,
|
167 |
+
mp_pose.POSE_CONNECTIONS,
|
168 |
+
mp_drawing.DrawingSpec(color=(0, 255, 0), thickness=1, circle_radius=2), # Green for upper arm
|
169 |
+
mp_drawing.DrawingSpec(color=(255, 0, 0), thickness=1, circle_radius=2) # Red for lower arm
|
170 |
+
)
|
171 |
+
|
172 |
+
if angle_knee < 90:
|
173 |
+
current_rep_issues["knees_bending_too_much"] = True
|
174 |
+
cv2.line(guide_image, tuple(np.multiply(knee, [1, 1]).astype(int)),
|
175 |
+
tuple(np.multiply(ankle, [1, 1]).astype(int)), (0, 255, 255), 2)
|
176 |
+
if angle_hip < 80:
|
177 |
+
current_rep_issues["hips_bending_too_much"] = True
|
178 |
+
cv2.line(guide_image, tuple(np.multiply(hip, [1, 1]).astype(int)),
|
179 |
+
tuple(np.multiply(knee, [1, 1]).astype(int)), (0, 0, 255), 2)
|
180 |
+
if angle_back < 70:
|
181 |
+
current_rep_issues["back_leaning_too_much"] = True
|
182 |
+
cv2.line(guide_image, tuple(np.multiply(shoulder, [1, 1]).astype(int)),
|
183 |
+
tuple(np.multiply(hip, [1, 1]).astype(int)), (255, 0, 0), 2)
|
184 |
+
|
185 |
+
# Rep counting logic
|
186 |
+
if angle_knee > 160 and stage != "UP":
|
187 |
+
stage = "UP"
|
188 |
+
for issue, occurred in current_rep_issues.items():
|
189 |
+
if occurred:
|
190 |
+
form_issues[issue] += 1
|
191 |
+
rep_scores.append(calculate_rep_score(
|
192 |
+
current_rep_angles['knee'],
|
193 |
+
current_rep_angles['hip'],
|
194 |
+
current_rep_angles['back']
|
195 |
+
))
|
196 |
+
current_rep_angles = {'knee': [], 'hip': [], 'back': []}
|
197 |
+
current_rep_issues = {k: False for k in current_rep_issues}
|
198 |
+
elif angle_knee <= 90 and stage == "UP":
|
199 |
+
stage = "DOWN"
|
200 |
+
squat_count += 1
|
201 |
+
squat_count_placeholder.write(f"Squat Count: {squat_count}")
|
202 |
+
|
203 |
+
# Update video streams
|
204 |
+
original_video.image(frame, channels="BGR", use_column_width=True)
|
205 |
+
points_video.image(points_image, channels="BGR", use_column_width=True)
|
206 |
+
guide_video.image(guide_image, channels="BGR", use_column_width=True)
|
207 |
+
|
208 |
+
cap.release()
|
209 |
+
end_time = time.time()
|
210 |
+
analysis_time = end_time - start_time
|
211 |
+
|
212 |
+
# Generate and display the report
|
213 |
+
report = generate_workout_report(rep_scores, form_issues, analysis_time)
|
214 |
+
feedback_placeholder.text(report)
|
pose_landmarks_index.png
ADDED
requirements.txt
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
opencv-python-headless
|
2 |
+
mediapipe
|
3 |
+
numpy
|
4 |
+
streamlit
|
5 |
+
numpy
|
6 |
+
streamlit_webrtc
|
7 |
+
streamlit-lottie
|