Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -3,7 +3,7 @@ import cv2
|
|
3 |
import numpy as np
|
4 |
import mediapipe as mp
|
5 |
import time
|
6 |
-
import traceback
|
7 |
|
8 |
# Import your exercise classes
|
9 |
from exercises.hammer_curl import HammerCurl
|
@@ -22,135 +22,19 @@ exercise_trackers = {
|
|
22 |
"Push Up": PushUp(),
|
23 |
"Squat": Squat()
|
24 |
}
|
25 |
-
|
26 |
-
# It's fetched using exercise_trackers.get(selected_exercise_name)
|
27 |
selected_exercise_name = "Hammer Curl" # Default exercise
|
28 |
|
29 |
-
|
30 |
-
|
|
|
31 |
current_set_count = 1
|
32 |
workout_complete_message = ""
|
33 |
-
workout_active = False # New state variable to control active workout session
|
34 |
|
35 |
|
36 |
-
def
|
37 |
-
|
38 |
-
|
39 |
-
Updates system state and prepares for a new workout session if one isn't active.
|
40 |
-
Returns values to update relevant UI text components.
|
41 |
-
"""
|
42 |
-
global selected_exercise_name, target_reps, target_sets
|
43 |
-
global current_set_count, workout_complete_message, workout_active
|
44 |
-
|
45 |
-
exercise_changed_or_targets_changed = False
|
46 |
-
|
47 |
-
# Update selected exercise
|
48 |
-
if selected_exercise_name != exercise_name_choice:
|
49 |
-
selected_exercise_name = exercise_name_choice
|
50 |
-
exercise_changed_or_targets_changed = True
|
51 |
-
print(f"Exercise changed to: {selected_exercise_name}.")
|
52 |
-
|
53 |
-
# Update target reps
|
54 |
-
try:
|
55 |
-
new_reps = int(reps_in)
|
56 |
-
if new_reps > 0 and target_reps != new_reps:
|
57 |
-
target_reps = new_reps
|
58 |
-
exercise_changed_or_targets_changed = True
|
59 |
-
print(f"Target reps updated to: {target_reps}.")
|
60 |
-
except (ValueError, TypeError): pass # Keep old value if input is not a valid integer
|
61 |
-
|
62 |
-
# Update target sets
|
63 |
-
try:
|
64 |
-
new_sets = int(sets_in)
|
65 |
-
if new_sets > 0 and target_sets != new_sets:
|
66 |
-
target_sets = new_sets
|
67 |
-
exercise_changed_or_targets_changed = True
|
68 |
-
print(f"Target sets updated to: {target_sets}.")
|
69 |
-
except (ValueError, TypeError): pass
|
70 |
-
|
71 |
-
if exercise_changed_or_targets_changed:
|
72 |
-
# Reset tracker and progress as exercise or targets changed
|
73 |
-
current_tracker = exercise_trackers.get(selected_exercise_name)
|
74 |
-
if current_tracker: # Ensure tracker exists before trying to reset
|
75 |
-
if selected_exercise_name == "Hammer Curl": exercise_trackers[selected_exercise_name] = HammerCurl()
|
76 |
-
elif selected_exercise_name == "Push Up": exercise_trackers[selected_exercise_name] = PushUp()
|
77 |
-
elif selected_exercise_name == "Squat": exercise_trackers[selected_exercise_name] = Squat()
|
78 |
-
# No need to call reset_reps() here as we just re-instantiated.
|
79 |
-
current_set_count = 1
|
80 |
-
workout_complete_message = ""
|
81 |
-
workout_active = False # Changing exercise or targets stops any active workout
|
82 |
-
print(f"Config changed. Workout stopped and progress reset for {selected_exercise_name}.")
|
83 |
-
|
84 |
-
# Determine initial display values based on current (possibly reset) state
|
85 |
-
current_reps_val = 0
|
86 |
-
# Fetch the tracker again after potential re-instantiation
|
87 |
-
display_tracker = exercise_trackers.get(selected_exercise_name)
|
88 |
-
if display_tracker:
|
89 |
-
if hasattr(display_tracker, 'counter'): # For Pushup/Squat
|
90 |
-
current_reps_val = display_tracker.counter
|
91 |
-
elif hasattr(display_tracker, 'counter_right'): # For Hammer Curl
|
92 |
-
current_reps_val = display_tracker.counter_right
|
93 |
-
|
94 |
-
reps_disp = f"{current_reps_val}/{target_reps}"
|
95 |
-
if selected_exercise_name == "Hammer Curl" and display_tracker:
|
96 |
-
r_c = display_tracker.counter_right
|
97 |
-
l_c = display_tracker.counter_left
|
98 |
-
reps_disp = f"R: {r_c}, L: {l_c} (Target: {target_reps} for R)"
|
99 |
-
|
100 |
-
initial_feedback = "Select exercise, set targets, then press 'Start Workout'."
|
101 |
-
if workout_active: # Should not be active after config change, but for safety
|
102 |
-
initial_feedback = "Processing..."
|
103 |
-
elif workout_complete_message:
|
104 |
-
initial_feedback = workout_complete_message
|
105 |
-
|
106 |
-
current_workout_status = workout_complete_message if workout_complete_message else ("Workout Not Active" if not workout_active else "Workout Active")
|
107 |
-
|
108 |
-
return (selected_exercise_name, # For current_exercise_display
|
109 |
-
reps_disp, # For reps_output
|
110 |
-
f"{current_set_count}/{target_sets}", # For sets_output
|
111 |
-
initial_feedback, # For feedback_output
|
112 |
-
current_workout_status)# For workout_status_output
|
113 |
-
|
114 |
-
|
115 |
-
def trigger_start_workout():
|
116 |
-
global current_set_count, workout_complete_message, workout_active, selected_exercise_name, target_reps, target_sets
|
117 |
-
|
118 |
-
print("Start Workout button clicked.")
|
119 |
-
workout_active = True
|
120 |
-
current_set_count = 1
|
121 |
-
workout_complete_message = ""
|
122 |
-
|
123 |
-
# Ensure the correct tracker is instantiated and reset for the selected exercise
|
124 |
-
if selected_exercise_name == "Hammer Curl": exercise_trackers[selected_exercise_name] = HammerCurl()
|
125 |
-
elif selected_exercise_name == "Push Up": exercise_trackers[selected_exercise_name] = PushUp()
|
126 |
-
elif selected_exercise_name == "Squat": exercise_trackers[selected_exercise_name] = Squat()
|
127 |
-
# No need to call reset_reps() on a newly instantiated object.
|
128 |
-
|
129 |
-
print(f"Tracker for {selected_exercise_name} (re)initialized for start.")
|
130 |
-
|
131 |
-
reps_disp = f"0/{target_reps}"
|
132 |
-
if selected_exercise_name == "Hammer Curl":
|
133 |
-
reps_disp = f"R: 0, L: 0 (Target: {target_reps} for R)"
|
134 |
-
|
135 |
-
return (selected_exercise_name,
|
136 |
-
reps_disp,
|
137 |
-
f"1/{target_sets}",
|
138 |
-
f"Workout Started: {selected_exercise_name}. Go!",
|
139 |
-
"Workout Active")
|
140 |
-
|
141 |
-
def trigger_stop_workout():
|
142 |
-
global workout_active
|
143 |
-
print("Stop Workout button clicked.")
|
144 |
-
workout_active = False
|
145 |
-
return ("Workout Stopped. Press Start to resume or change settings.", # Feedback
|
146 |
-
"Workout Stopped") # Status
|
147 |
-
|
148 |
-
|
149 |
-
def process_frame(video_frame_np):
|
150 |
-
global selected_exercise_name, target_reps, target_sets # Read-only in this part
|
151 |
-
global current_set_count, workout_complete_message, workout_active # Read/Write
|
152 |
-
|
153 |
-
current_exercise_tracker = exercise_trackers.get(selected_exercise_name) # Get current tracker
|
154 |
|
155 |
default_h, default_w = 480, 640
|
156 |
if video_frame_np is not None:
|
@@ -159,296 +43,250 @@ def process_frame(video_frame_np):
|
|
159 |
else:
|
160 |
blank_frame = np.zeros((default_h, default_w, 3), dtype=np.uint8)
|
161 |
cv2.putText(blank_frame, "No Camera Input", (50, default_h // 2), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 2)
|
162 |
-
return blank_frame, f"0/{target_reps}", f"{current_set_count}/{target_sets}", "
|
163 |
-
|
164 |
-
# Initialize display values based on current state (might be immediately overwritten)
|
165 |
-
reps_val_display = 0
|
166 |
-
if current_exercise_tracker:
|
167 |
-
if hasattr(current_exercise_tracker, 'counter'): reps_val_display = current_exercise_tracker.counter
|
168 |
-
elif hasattr(current_exercise_tracker, 'counter_right'): reps_val_display = current_exercise_tracker.counter_right
|
169 |
-
|
170 |
-
reps_display = f"{reps_val_display}/{target_reps}"
|
171 |
-
if selected_exercise_name == "Hammer Curl" and current_exercise_tracker:
|
172 |
-
reps_display = f"R: {current_exercise_tracker.counter_right}, L: {current_exercise_tracker.counter_left} (Target: {target_reps} for R)"
|
173 |
-
|
174 |
-
sets_display = f"{current_set_count}/{target_sets}"
|
175 |
-
angle_display = "N/A" # Default, will be updated if processing occurs
|
176 |
-
feedback_display = "Waiting..." # Default
|
177 |
-
current_workout_status = "Not Active" # Default
|
178 |
|
179 |
try:
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
-
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
if
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
248 |
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
263 |
elif workout_complete_message:
|
264 |
feedback_display = workout_complete_message
|
265 |
-
current_workout_status = workout_complete_message
|
266 |
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)"
|
267 |
-
sets_display = f"{target_sets}/{target_sets}"
|
268 |
-
|
269 |
-
if results and results.pose_landmarks: # results might be from a previous frame if workout_active just became false
|
270 |
mp_drawing.draw_landmarks(annotated_image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS, landmark_drawing_spec=mp_drawing_styles.get_default_pose_landmarks_style())
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
feedback_display = "Workout stopped or not started. Press 'Start Workout'."
|
275 |
-
current_workout_status = "Workout Stopped / Not Started"
|
276 |
-
# Draw generic landmarks if pose was processed (results might be available)
|
277 |
if results and results.pose_landmarks :
|
278 |
mp_drawing.draw_landmarks(annotated_image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS, landmark_drawing_spec=mp_drawing_styles.get_default_pose_landmarks_style())
|
279 |
-
else:
|
280 |
-
|
281 |
-
|
|
|
282 |
|
283 |
-
# Final check on annotated_image before returning
|
284 |
if not isinstance(annotated_image, np.ndarray) or annotated_image.ndim != 3 or annotated_image.shape[2] != 3:
|
285 |
-
|
286 |
-
annotated_image = np.zeros((default_h, default_w, 3), dtype=np.uint8) # Fallback
|
287 |
cv2.putText(annotated_image, "Display Error", (50, default_h // 2), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255),2)
|
288 |
-
|
289 |
-
return annotated_image, reps_display, sets_display, angle_display, feedback_display,
|
290 |
|
291 |
except Exception as e_main:
|
292 |
print(f"PROCESS_FRAME: CRITICAL error in process_frame: {e_main}")
|
293 |
print(traceback.format_exc())
|
294 |
error_frame = np.zeros((default_h, default_w, 3), dtype=np.uint8)
|
295 |
-
cv2.putText(error_frame, "
|
296 |
-
|
297 |
-
return error_frame, "Error", "Error", "Error", "Critical Error", "Error"
|
298 |
|
299 |
-
|
300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
301 |
custom_css = """
|
302 |
-
body
|
303 |
-
background: linear-gradient(to bottom right, #
|
304 |
-
|
|
|
|
|
305 |
}
|
306 |
-
.gradio-container { font-family: 'Exo 2', sans-serif !important; }
|
307 |
label, .gr-checkbox-label span { /* Target labels and checkbox labels */
|
308 |
-
color: #
|
309 |
font-weight: bold !important;
|
310 |
}
|
311 |
-
|
312 |
-
.gradio-group > .gr-box > .gr-markdown > div > h3, /* For gr.Markdown("### Title") inside gr.Group */
|
313 |
-
.gr-form > .gr-markdown > div > h3 { /* For gr.Markdown("### Title") as direct child of form/column */
|
314 |
-
color: #FFFFFF !important;
|
315 |
-
text-align: left !important; /* Titles in sections are usually left-aligned */
|
316 |
-
font-family: 'Exo 2', sans-serif !important;
|
317 |
-
margin-bottom: 0.5em !important; /* Add some space below section titles */
|
318 |
-
}
|
319 |
-
/* Main Page Title */
|
320 |
-
.gradio-container > .main > .wrap > .contain > . ΟΟΞΏΟ
> .output > .gr-markdown > div > h1, /* More specific for main H1 */
|
321 |
-
.gradio-container > div > .gr-markdown > div > h1 { /* Fallback for main H1 */
|
322 |
color: #FFFFFF !important;
|
323 |
text-align: center !important;
|
324 |
-
font-family: 'Exo 2', sans-serif !important;
|
325 |
-
font-size: 2.5em !important; /* Larger main title */
|
326 |
-
margin-top: 0.5em !important;
|
327 |
-
margin-bottom: 0.2em !important;
|
328 |
-
}
|
329 |
-
/* Subtitle Markdown */
|
330 |
-
.gradio-container > .main > .wrap > .contain > . ΟΟΞΏΟ
> .output > .gr-markdown:nth-of-type(2) > div > p,
|
331 |
-
.gradio-container > div > .gr-markdown:nth-of-type(2) > div > p {
|
332 |
-
color: #E8E8E8 !important;
|
333 |
-
text-align: center !important;
|
334 |
-
font-size: 1.1em !important;
|
335 |
-
margin-bottom: 1.5em !important;
|
336 |
}
|
337 |
-
.
|
338 |
-
|
339 |
-
border-radius: 8px !important;
|
340 |
-
font-weight: bold !important;
|
341 |
}
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
border: 1px solid rgba(255,255,255,0.1) !important;
|
346 |
-
padding: 15px !important;
|
347 |
-
margin-bottom: 15px !important;
|
348 |
}
|
349 |
-
|
350 |
-
|
351 |
-
|
|
|
352 |
}
|
353 |
"""
|
354 |
|
355 |
# --- Gradio Theme ---
|
|
|
|
|
356 |
theme = gr.themes.Base(
|
357 |
font=[gr.themes.GoogleFont("Exo 2"), "ui-sans-serif", "system-ui", "sans-serif"],
|
358 |
-
primary_hue=gr.themes.colors.
|
359 |
-
secondary_hue=gr.themes.colors.
|
360 |
-
neutral_hue=gr.themes.colors.
|
361 |
).set(
|
362 |
-
body_text_color="#E0E0E0", #
|
363 |
-
input_background_fill="
|
364 |
-
input_border_color="
|
365 |
-
#
|
366 |
-
|
367 |
-
button_primary_background_fill_hover=gr.themes.colors.pink,
|
368 |
-
button_primary_text_color="#FFFFFF",
|
369 |
-
button_secondary_background_fill=gr.themes.colors.pink,
|
370 |
-
button_secondary_background_fill_hover=gr.themes.colors.purple,
|
371 |
-
button_secondary_text_color="#FFFFFF",
|
372 |
-
block_title_text_color = "#FFFFFF",
|
373 |
-
block_label_text_color = "#E0E0E0",
|
374 |
-
border_color_accent = gr.themes.colors.purple,
|
375 |
-
background_fill_primary = "#1E001E",
|
376 |
-
background_fill_secondary = "#2A0A2A",
|
377 |
)
|
|
|
|
|
378 |
# --- Gradio Interface ---
|
379 |
-
|
380 |
|
|
|
381 |
with gr.Blocks(theme=theme, css=custom_css) as iface:
|
382 |
-
gr.Markdown("#
|
383 |
-
gr.Markdown("
|
384 |
|
385 |
-
with gr.Row(
|
386 |
-
with gr.Column(scale=2):
|
387 |
-
webcam_input = gr.Image(sources=["webcam"], streaming=True, type="numpy", label="
|
388 |
-
|
389 |
with gr.Column(scale=1):
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
pu_btn = gr.Button("Push Up", scale=1)
|
395 |
-
sq_btn = gr.Button("Squat", scale=1)
|
396 |
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
gr.Markdown("### Current Status")
|
408 |
-
current_exercise_display = gr.Textbox(label="Exercise:", value=selected_exercise_name, interactive=False)
|
409 |
-
sets_output = gr.Textbox(label="Set:", interactive=False)
|
410 |
-
reps_output = gr.Textbox(label="Repetitions:", interactive=False)
|
411 |
-
angle_output = gr.Textbox(label="Angle Details:", interactive=False) # <-- Add this line back
|
412 |
-
feedback_output = gr.Textbox(label="Feedback:", lines=3, max_lines=5, interactive=False)
|
413 |
-
workout_status_output = gr.Textbox(label="Workout Status:", interactive=False)
|
414 |
-
|
415 |
-
# Define outputs that are updated by multiple control changes (exercise selection, target changes)
|
416 |
-
# These are: current exercise display, reps, sets, feedback, workout status
|
417 |
-
shared_ui_outputs = [current_exercise_display, reps_output, sets_output, feedback_output, workout_status_output]
|
418 |
-
|
419 |
-
# Outputs from the main process_frame (includes video + all text fields for status)
|
420 |
process_frame_outputs = [webcam_input, reps_output, sets_output, angle_output, feedback_output, workout_status_output]
|
421 |
|
|
|
|
|
|
|
|
|
422 |
|
423 |
-
|
424 |
-
|
425 |
-
def handler(reps, sets):
|
426 |
-
# Call the main update function
|
427 |
-
return update_targets_and_exercise_display(exercise_name_to_select, reps, sets)
|
428 |
-
return handler
|
429 |
-
|
430 |
-
# Exercise selection buttons
|
431 |
-
hc_btn.click(create_config_change_handler("Hammer Curl"), inputs=[target_reps_number, target_sets_number], outputs=shared_ui_outputs)
|
432 |
-
pu_btn.click(create_config_change_handler("Push Up"), inputs=[target_reps_number, target_sets_number], outputs=shared_ui_outputs)
|
433 |
-
sq_btn.click(create_config_change_handler("Squat"), inputs=[target_reps_number, target_sets_number], outputs=shared_ui_outputs)
|
434 |
-
|
435 |
-
# Target number changes also call update_targets_and_exercise_display
|
436 |
-
# They need to know the currently selected exercise to pass it correctly
|
437 |
-
# For this, we'll pass selected_exercise_name (which is global and updated by buttons)
|
438 |
-
# The target_reps_number and target_sets_number inputs will be their own current values
|
439 |
-
target_reps_number.change(lambda r, s: update_targets_and_exercise_display(selected_exercise_name, r, s),
|
440 |
-
inputs=[target_reps_number, target_sets_number],
|
441 |
-
outputs=shared_ui_outputs)
|
442 |
-
target_sets_number.change(lambda r, s: update_targets_and_exercise_display(selected_exercise_name, r, s),
|
443 |
-
inputs=[target_reps_number, target_sets_number],
|
444 |
-
outputs=shared_ui_outputs)
|
445 |
-
|
446 |
-
# Start and Stop buttons
|
447 |
-
start_button.click(trigger_start_workout, inputs=None, outputs=shared_ui_outputs)
|
448 |
-
stop_button.click(trigger_stop_workout, inputs=None, outputs=[feedback_output, workout_status_output])
|
449 |
-
|
450 |
-
# Video stream processing
|
451 |
-
webcam_input.stream(fn=process_frame, inputs=[webcam_input], outputs=process_frame_outputs)
|
452 |
|
453 |
if __name__ == "__main__":
|
454 |
-
iface.launch(debug=False, share=False)
|
|
|
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
|
|
|
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:
|
|
|
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
|