pjxcharya commited on
Commit
20ac831
Β·
verified Β·
1 Parent(s): 051f9c9

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +292 -0
  2. requirements.txt +4 -0
app.py ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import cv2
3
+ import numpy as np
4
+ import mediapipe as mp
5
+ import time
6
+ import traceback # For detailed error logging
7
+
8
+ # Import your exercise classes
9
+ from exercises.hammer_curl import HammerCurl
10
+ from exercises.push_up import PushUp
11
+ from exercises.squat import Squat
12
+
13
+ # Initialize MediaPipe Pose
14
+ mp_pose = mp.solutions.pose
15
+ pose = mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5)
16
+ mp_drawing = mp.solutions.drawing_utils
17
+ mp_drawing_styles = mp.solutions.drawing_styles
18
+
19
+ # --- State Variables ---
20
+ exercise_trackers = {
21
+ "Hammer Curl": HammerCurl(),
22
+ "Push Up": PushUp(),
23
+ "Squat": Squat()
24
+ }
25
+ current_exercise_tracker = None
26
+ selected_exercise_name = "Hammer Curl" # Default exercise
27
+
28
+ # Target and progress tracking
29
+ target_reps = 10 # Default target reps
30
+ target_sets = 3 # Default target sets
31
+ current_set_count = 1
32
+ workout_complete_message = ""
33
+
34
+
35
+ def process_frame(video_frame_np, exercise_name_choice, target_reps_input, target_sets_input):
36
+ global current_exercise_tracker, selected_exercise_name
37
+ global target_reps, target_sets, current_set_count, workout_complete_message
38
+
39
+ default_h, default_w = 480, 640
40
+ if video_frame_np is not None:
41
+ default_h, default_w, _ = video_frame_np.shape
42
+ annotated_image = video_frame_np.copy()
43
+ else:
44
+ blank_frame = np.zeros((default_h, default_w, 3), dtype=np.uint8)
45
+ cv2.putText(blank_frame, "No Camera Input", (50, default_h // 2), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 2)
46
+ return blank_frame, f"0/{target_reps}", f"{current_set_count}/{target_sets}", "No frame", "No camera", "Error: No Frame"
47
+
48
+ try:
49
+ new_target_reps_val = target_reps
50
+ try:
51
+ new_target_reps_val = int(target_reps_input)
52
+ if new_target_reps_val > 0 and target_reps != new_target_reps_val:
53
+ target_reps = new_target_reps_val
54
+ if current_exercise_tracker: current_exercise_tracker.reset_reps()
55
+ current_set_count = 1
56
+ workout_complete_message = ""
57
+ except ValueError: pass
58
+
59
+ new_target_sets_val = target_sets
60
+ try:
61
+ new_target_sets_val = int(target_sets_input)
62
+ if new_target_sets_val > 0 and target_sets != new_target_sets_val:
63
+ target_sets = new_target_sets_val
64
+ if current_exercise_tracker: current_exercise_tracker.reset_reps()
65
+ current_set_count = 1
66
+ workout_complete_message = ""
67
+ except ValueError: pass
68
+
69
+ if selected_exercise_name != exercise_name_choice:
70
+ selected_exercise_name = exercise_name_choice
71
+ if selected_exercise_name in exercise_trackers:
72
+ if selected_exercise_name == "Hammer Curl": exercise_trackers[selected_exercise_name] = HammerCurl()
73
+ elif selected_exercise_name == "Push Up": exercise_trackers[selected_exercise_name] = PushUp()
74
+ elif selected_exercise_name == "Squat": exercise_trackers[selected_exercise_name] = Squat()
75
+ current_set_count = 1
76
+ workout_complete_message = ""
77
+ else: current_exercise_tracker = None
78
+ current_exercise_tracker = exercise_trackers.get(selected_exercise_name)
79
+
80
+ image_rgb = cv2.cvtColor(video_frame_np, cv2.COLOR_BGR2RGB)
81
+ image_rgb.flags.writeable = False
82
+ results = pose.process(image_rgb)
83
+ image_rgb.flags.writeable = True
84
+
85
+ reps_display = f"0/{target_reps}"
86
+ sets_display = f"{current_set_count}/{target_sets}"
87
+ angle_display = "N/A"
88
+ feedback_display = "Initializing..."
89
+ temp_workout_message = workout_complete_message
90
+
91
+ if results.pose_landmarks and current_exercise_tracker and not workout_complete_message:
92
+ landmarks_mp = results.pose_landmarks.landmark
93
+ frame_height, frame_width, _ = annotated_image.shape
94
+ actual_reps_this_set = 0
95
+
96
+ try:
97
+ if selected_exercise_name == "Hammer Curl":
98
+ r_count, r_angle, l_count, l_angle, warn_r, warn_l, _, _, r_stage, l_stage = current_exercise_tracker.track_hammer_curl(landmarks_mp, annotated_image)
99
+ actual_reps_this_set = r_count
100
+ reps_display = f"R: {r_count}, L: {l_count} (Target: {target_reps} for R)"
101
+ angle_display = f"R Ang: {int(r_angle)}, L Ang: {int(l_angle)}"
102
+ feedback_list = []
103
+ if warn_r: feedback_list.append(f"R: {warn_r}")
104
+ if warn_l: feedback_list.append(f"L: {warn_l}")
105
+ feedback_display = " | ".join(feedback_list) if feedback_list else "Good form!"
106
+
107
+ elif selected_exercise_name == "Push Up":
108
+ exercise_data = current_exercise_tracker.track_push_up(landmarks_mp, frame_width, frame_height)
109
+ actual_reps_this_set = exercise_data.get("counter", 0)
110
+ angle_display = f"L: {int(exercise_data.get('angle_left',0))}, R: {int(exercise_data.get('angle_right',0))}"
111
+ feedback_display = str(exercise_data.get("feedback", "No feedback"))
112
+ if 'get_drawing_annotations' in dir(current_exercise_tracker):
113
+ annotations_to_draw = current_exercise_tracker.get_drawing_annotations(landmarks_mp, frame_width, frame_height, exercise_data)
114
+ for ann in annotations_to_draw:
115
+ if ann["type"] == "line": cv2.line(annotated_image, tuple(ann["start_point"]), tuple(ann["end_point"]), ann["color_bgr"], ann["thickness"])
116
+ elif ann["type"] == "circle": cv2.circle(annotated_image, tuple(ann["center_point"]), ann["radius"], ann["color_bgr"], -1 if ann.get("filled", False) else ann["thickness"])
117
+ elif ann["type"] == "text": cv2.putText(annotated_image, ann["text_content"], tuple(ann["position"]), cv2.FONT_HERSHEY_SIMPLEX, ann["font_scale"], ann["color_bgr"], ann["thickness"])
118
+
119
+ elif selected_exercise_name == "Squat":
120
+ exercise_data = current_exercise_tracker.track_squat(landmarks_mp, frame_width, frame_height)
121
+ actual_reps_this_set = exercise_data.get("counter", 0)
122
+ angle_display = f"L: {int(exercise_data.get('angle_left',0))}, R: {int(exercise_data.get('angle_right',0))}"
123
+ feedback_display = str(exercise_data.get("feedback", "No feedback"))
124
+ if 'get_drawing_annotations' in dir(current_exercise_tracker):
125
+ annotations_to_draw = current_exercise_tracker.get_drawing_annotations(landmarks_mp, frame_width, frame_height, exercise_data)
126
+ for ann in annotations_to_draw:
127
+ if ann["type"] == "line": cv2.line(annotated_image, tuple(ann["start_point"]), tuple(ann["end_point"]), ann["color_bgr"], ann["thickness"])
128
+ elif ann["type"] == "circle": cv2.circle(annotated_image, tuple(ann["center_point"]), ann["radius"], ann["color_bgr"], -1 if ann.get("filled", False) else ann["thickness"])
129
+ elif ann["type"] == "text": cv2.putText(annotated_image, ann["text_content"], tuple(ann["position"]), cv2.FONT_HERSHEY_SIMPLEX, ann["font_scale"], ann["color_bgr"], ann["thickness"])
130
+
131
+ if selected_exercise_name != "Hammer Curl":
132
+ reps_display = f"{actual_reps_this_set}/{target_reps}"
133
+
134
+ if actual_reps_this_set >= target_reps:
135
+ if current_set_count < target_sets:
136
+ current_set_count += 1
137
+ current_exercise_tracker.reset_reps()
138
+ feedback_display = f"Set {current_set_count-1} complete! Starting set {current_set_count}."
139
+ if selected_exercise_name == "Hammer Curl": reps_display = f"R: 0, L: 0 (Target: {target_reps} for R)"
140
+ else: reps_display = f"0/{target_reps}"
141
+ elif current_set_count >= target_sets:
142
+ feedback_display = "Workout Complete!"
143
+ workout_complete_message = "Workout Complete! Change targets or exercise to restart."
144
+ if selected_exercise_name == "Hammer Curl": reps_display = f"R: {target_reps}, L: {target_reps} (Target: {target_reps} for R)"
145
+ else: reps_display = f"{target_reps}/{target_reps}"
146
+ temp_workout_message = workout_complete_message
147
+
148
+ except Exception as e_exercise:
149
+ print(f"PROCESS_FRAME: Error during exercise '{selected_exercise_name}' logic: {e_exercise}")
150
+ print(traceback.format_exc())
151
+ cv2.putText(annotated_image, f"Error in {selected_exercise_name}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
152
+ feedback_display = f"Error in {selected_exercise_name} processing."
153
+
154
+ elif workout_complete_message:
155
+ feedback_display = workout_complete_message
156
+ reps_display = f"{target_reps}/{target_reps}" if selected_exercise_name != "Hammer Curl" else f"R: {target_reps}, L: {target_reps} (Target: {target_reps} for R)"
157
+ sets_display = f"{target_sets}/{target_sets}"
158
+ if results.pose_landmarks:
159
+ mp_drawing.draw_landmarks(annotated_image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS, landmark_drawing_spec=mp_drawing_styles.get_default_pose_landmarks_style())
160
+
161
+ else:
162
+ feedback_display = "No person detected or exercise not selected properly."
163
+ if results and results.pose_landmarks :
164
+ mp_drawing.draw_landmarks(annotated_image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS, landmark_drawing_spec=mp_drawing_styles.get_default_pose_landmarks_style())
165
+ else:
166
+ cv2.putText(annotated_image, "No person detected", (50, default_h // 2), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255),2)
167
+
168
+ sets_display = f"{current_set_count}/{target_sets}"
169
+
170
+ if not isinstance(annotated_image, np.ndarray) or annotated_image.ndim != 3 or annotated_image.shape[2] != 3:
171
+ annotated_image = np.zeros((default_h, default_w, 3), dtype=np.uint8)
172
+ cv2.putText(annotated_image, "Display Error", (50, default_h // 2), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255),2)
173
+
174
+ return annotated_image, reps_display, sets_display, angle_display, feedback_display, temp_workout_message
175
+
176
+ except Exception as e_main:
177
+ print(f"PROCESS_FRAME: CRITICAL error in process_frame: {e_main}")
178
+ print(traceback.format_exc())
179
+ error_frame = np.zeros((default_h, default_w, 3), dtype=np.uint8)
180
+ cv2.putText(error_frame, f"Error: {e_main}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
181
+ return error_frame, "Error", "Error", "Error", "Critical Error", "Critical Error"
182
+
183
+ def trigger_reset_workout():
184
+ global current_set_count, workout_complete_message, selected_exercise_name, target_reps, target_sets, current_exercise_tracker
185
+
186
+ current_set_count = 1
187
+ workout_complete_message = ""
188
+
189
+ if current_exercise_tracker:
190
+ current_exercise_tracker.reset_reps()
191
+
192
+ reset_reps_display = f"0/{target_reps}"
193
+ if selected_exercise_name == "Hammer Curl":
194
+ reset_reps_display = f"R: 0, L: 0 (Target: {target_reps} for R)"
195
+
196
+ reset_sets_display = f"1/{target_sets}"
197
+ reset_angle_display = "N/A"
198
+ reset_feedback_display = "Workout Reset. Ready to start."
199
+ reset_workout_status = ""
200
+
201
+ return reset_reps_display, reset_sets_display, reset_angle_display, reset_feedback_display, reset_workout_status
202
+
203
+ # --- Custom CSS for gradient background and styling ---
204
+ # Note: Applying gradient to the root/body might be overridden by Gradio's specific block styling.
205
+ # It's often better to target .gradio-container or specific blocks if possible.
206
+ # However, for broad effect, body is a start. More specific selectors might be needed for full coverage.
207
+ # --- Custom CSS for gradient background and styling ---
208
+ custom_css = """
209
+ body {
210
+ background: linear-gradient(to bottom right, #301934, #8A2BE0) !important; /* Dark Violet to a brighter Violet */
211
+ }
212
+ .gradio-container {
213
+ background: linear-gradient(to bottom right, #301934, #8A2BE0) !important; /* Ensure container also gets it */
214
+ }
215
+ label, .gr-checkbox-label span { /* Target labels and checkbox labels */
216
+ color: #E8E8E8 !important; /* Slightly brighter light gray for labels */
217
+ font-weight: bold !important;
218
+ }
219
+ h1 {
220
+ color: #FFFFFF !important;
221
+ text-align: center !important;
222
+ }
223
+ .prose { /* Markdown text */
224
+ color: #F0F0F0 !important;
225
+ }
226
+ /* General text within blocks that isn't a label or title */
227
+ .gr-block .gr-box > div > p, .gr-block .gr-box > div > span {
228
+ color: #E0E0E0 !important;
229
+ }
230
+ /* You might need to adjust button font styling with more specific selectors if this doesn't work */
231
+ button.gr-button-primary {
232
+ font-family: 'Exo 2', sans-serif !important;
233
+ /* The theme's primary_hue should handle button background and text color for contrast */
234
+ }
235
+ """
236
+
237
+ # --- Gradio Theme ---
238
+ # Using a base theme to set font and then overriding some colors.
239
+ # For button color, if primary_hue doesn't give desired button color, specific CSS might be needed.
240
+ theme = gr.themes.Base(
241
+ font=[gr.themes.GoogleFont("Exo 2"), "ui-sans-serif", "system-ui", "sans-serif"],
242
+ primary_hue=gr.themes.colors.amber, # For buttons - gives a yellowish/golden hue
243
+ secondary_hue=gr.themes.colors.blue,
244
+ neutral_hue=gr.themes.colors.gray
245
+ ).set(
246
+ body_text_color="#E0E0E0", # Light gray for general text (this should also affect input text)
247
+ input_background_fill="#4A2A6C", # Darker violet for input backgrounds
248
+ input_border_color="#6A3AA2",
249
+ # button_primary_text_color="#111111", # Often better to let theme handle this or use CSS
250
+ # Ensure other text elements have good contrast automatically or via custom_css
251
+ )
252
+
253
+
254
+ # --- Gradio Interface ---
255
+ exercise_choices = ["Hammer Curl", "Push Up", "Squat"]
256
+
257
+ # Pass the theme and custom_css to gr.Blocks
258
+ with gr.Blocks(theme=theme, css=custom_css) as iface:
259
+ gr.Markdown("# Live AI Trainer") # This will be styled by H1 in CSS
260
+ gr.Markdown("Select an exercise, set your targets, and get real-time feedback on your form and reps.") # Styled by .prose
261
+
262
+ with gr.Row():
263
+ with gr.Column(scale=2):
264
+ webcam_input = gr.Image(sources=["webcam"], streaming=True, type="numpy", label="Your Webcam")
265
+ with gr.Column(scale=1):
266
+ gr.Markdown("### Controls") # Markdown default color should be handled by theme or prose
267
+ exercise_dropdown = gr.Dropdown(choices=exercise_choices, label="Select Exercise", value="Hammer Curl")
268
+ target_reps_number = gr.Number(value=target_reps, label="Target Reps per Set", precision=0, minimum=1)
269
+ target_sets_number = gr.Number(value=target_sets, label="Target Sets", precision=0, minimum=1)
270
+
271
+ reset_button = gr.Button("Reset Workout") # Will use primary_hue from theme
272
+
273
+ gr.Markdown("### Progress")
274
+ reps_output = gr.Textbox(label="Reps Progress")
275
+ sets_output = gr.Textbox(label="Sets Progress")
276
+ angle_output = gr.Textbox(label="Angle Details")
277
+ feedback_output = gr.Textbox(label="Feedback", lines=3, max_lines=5)
278
+ workout_status_output = gr.Textbox(label="Workout Status", lines=2, interactive=False)
279
+
280
+ process_frame_inputs = [webcam_input, exercise_dropdown, target_reps_number, target_sets_number]
281
+ process_frame_outputs = [webcam_input, reps_output, sets_output, angle_output, feedback_output, workout_status_output]
282
+
283
+ webcam_input.stream(fn=process_frame, inputs=process_frame_inputs, outputs=process_frame_outputs)
284
+ exercise_dropdown.change(fn=process_frame, inputs=process_frame_inputs, outputs=process_frame_outputs)
285
+ target_reps_number.change(fn=process_frame, inputs=process_frame_inputs, outputs=process_frame_outputs)
286
+ target_sets_number.change(fn=process_frame, inputs=process_frame_inputs, outputs=process_frame_outputs)
287
+
288
+ reset_button_outputs = [reps_output, sets_output, angle_output, feedback_output, workout_status_output]
289
+ reset_button.click(fn=trigger_reset_workout, inputs=None, outputs=reset_button_outputs)
290
+
291
+ if __name__ == "__main__":
292
+ iface.launch(debug=False, share=False) # share=False is default but good to be explicit for Spaces
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio
2
+ opencv-python
3
+ numpy
4
+ mediapipe