Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -10,7 +10,6 @@ import numpy as np
|
|
| 10 |
import matplotlib.pyplot as plt
|
| 11 |
import tempfile
|
| 12 |
import os
|
| 13 |
-
from dataclasses import dataclass
|
| 14 |
|
| 15 |
# Initialize MediaPipe
|
| 16 |
mp_pose = mp.solutions.pose
|
|
@@ -38,10 +37,10 @@ LANDMARKS = {
|
|
| 38 |
# Optimal angle ranges for bike fitting
|
| 39 |
OPTIMAL_RANGES = {
|
| 40 |
'torso_angle': (80, 90),
|
| 41 |
-
'hip_angle': (
|
| 42 |
-
'knee_angle': (
|
| 43 |
-
'ankle_angle': (90,
|
| 44 |
-
'elbow_angle': (150,
|
| 45 |
}
|
| 46 |
|
| 47 |
ANGLE_DESCRIPTIONS = {
|
|
@@ -209,82 +208,14 @@ def create_plots(angle_history):
|
|
| 209 |
return plot_path
|
| 210 |
|
| 211 |
|
| 212 |
-
def generate_recommendations(angle_history):
|
| 213 |
-
"""Generate bike fitting recommendations."""
|
| 214 |
-
if not angle_history:
|
| 215 |
-
return "No pose data detected. Please ensure the cyclist is visible in the video."
|
| 216 |
-
|
| 217 |
-
# Calculate statistics
|
| 218 |
-
stats = {}
|
| 219 |
-
for name in ['torso_angle', 'hip_angle', 'knee_angle', 'ankle_angle', 'elbow_angle']:
|
| 220 |
-
values = [a[name] for a in angle_history]
|
| 221 |
-
stats[name] = {
|
| 222 |
-
'mean': np.mean(values),
|
| 223 |
-
'min': np.min(values),
|
| 224 |
-
'max': np.max(values),
|
| 225 |
-
'std': np.std(values),
|
| 226 |
-
}
|
| 227 |
-
|
| 228 |
-
report = "## 🚴 Bike Fitting Analysis Report\n\n"
|
| 229 |
-
report += "### 📊 Angle Summary\n\n"
|
| 230 |
-
report += "| Angle | Mean | Range | Optimal | Status |\n"
|
| 231 |
-
report += "|-------|------|-------|---------|--------|\n"
|
| 232 |
-
|
| 233 |
-
for name, s in stats.items():
|
| 234 |
-
opt = OPTIMAL_RANGES.get(name, (0, 180))
|
| 235 |
-
mean = s['mean']
|
| 236 |
-
|
| 237 |
-
if opt[0] <= mean <= opt[1]:
|
| 238 |
-
status = "✅ Good"
|
| 239 |
-
elif mean < opt[0]:
|
| 240 |
-
status = f"⬆️ +{opt[0] - mean:.0f}°"
|
| 241 |
-
else:
|
| 242 |
-
status = f"⬇️ -{mean - opt[1]:.0f}°"
|
| 243 |
-
|
| 244 |
-
report += f"| {ANGLE_DESCRIPTIONS[name]} | {mean:.1f}° | {s['min']:.0f}°-{s['max']:.0f}° | {opt[0]}-{opt[1]}° | {status} |\n"
|
| 245 |
-
|
| 246 |
-
report += "\n### 💡 Recommendations\n\n"
|
| 247 |
-
|
| 248 |
-
# Knee angle recommendations
|
| 249 |
-
knee_max = stats['knee_angle']['max']
|
| 250 |
-
if knee_max < 140:
|
| 251 |
-
report += "**🔧 Saddle Height**: Consider **raising** the saddle. "
|
| 252 |
-
report += f"Your knee extension at the bottom of the stroke ({knee_max:.0f}°) is below optimal (140-148°). "
|
| 253 |
-
report += "This limits power output and may cause knee strain.\n\n"
|
| 254 |
-
elif knee_max > 155:
|
| 255 |
-
report += "**🔧 Saddle Height**: Consider **lowering** the saddle. "
|
| 256 |
-
report += f"Your knee is over-extending ({knee_max:.0f}°) which can cause injury.\n\n"
|
| 257 |
-
else:
|
| 258 |
-
report += "**✅ Saddle Height**: Looks good! Knee extension is within optimal range.\n\n"
|
| 259 |
-
|
| 260 |
-
# Torso angle recommendations
|
| 261 |
-
torso_mean = stats['torso_angle']['mean']
|
| 262 |
-
if torso_mean < 75:
|
| 263 |
-
report += "**🔧 Handlebar Position**: Very aggressive position. "
|
| 264 |
-
report += "Consider raising handlebars if you experience back or neck pain.\n\n"
|
| 265 |
-
elif torso_mean > 95:
|
| 266 |
-
report += "**🔧 Handlebar Position**: Upright position. "
|
| 267 |
-
report += "Good for comfort, but may reduce aerodynamic efficiency for racing.\n\n"
|
| 268 |
-
else:
|
| 269 |
-
report += "**✅ Torso Position**: Good balance of comfort and aerodynamics.\n\n"
|
| 270 |
-
|
| 271 |
-
# Hip angle recommendations
|
| 272 |
-
hip_min = stats['hip_angle']['min']
|
| 273 |
-
if hip_min < 35:
|
| 274 |
-
report += "**🔧 Saddle Position**: Hip angle closes too much at top of stroke. "
|
| 275 |
-
report += "Consider moving saddle back slightly.\n\n"
|
| 276 |
-
|
| 277 |
-
return report
|
| 278 |
-
|
| 279 |
-
|
| 280 |
def process_video(video_path, side, progress=gr.Progress()):
|
| 281 |
"""Main processing function."""
|
| 282 |
if video_path is None:
|
| 283 |
-
return None, None
|
| 284 |
|
| 285 |
cap = cv2.VideoCapture(video_path)
|
| 286 |
if not cap.isOpened():
|
| 287 |
-
return None, None
|
| 288 |
|
| 289 |
fps = int(cap.get(cv2.CAP_PROP_FPS)) or 30
|
| 290 |
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
@@ -300,7 +231,7 @@ def process_video(video_path, side, progress=gr.Progress()):
|
|
| 300 |
|
| 301 |
with mp_pose.Pose(
|
| 302 |
static_image_mode=False,
|
| 303 |
-
model_complexity=
|
| 304 |
min_detection_confidence=0.5,
|
| 305 |
min_tracking_confidence=0.5
|
| 306 |
) as pose:
|
|
@@ -339,11 +270,10 @@ def process_video(video_path, side, progress=gr.Progress()):
|
|
| 339 |
web_output = tempfile.mktemp(suffix='.mp4')
|
| 340 |
os.system(f'ffmpeg -y -i "{output_path}" -vcodec libx264 -acodec aac "{web_output}" -hide_banner -loglevel error')
|
| 341 |
|
| 342 |
-
# Generate
|
| 343 |
plot_path = create_plots(angle_history)
|
| 344 |
-
recommendations = generate_recommendations(angle_history)
|
| 345 |
|
| 346 |
-
return web_output, plot_path
|
| 347 |
|
| 348 |
|
| 349 |
# Build Gradio interface
|
|
@@ -365,7 +295,7 @@ with gr.Blocks(title="🚴 AI Bike Fitting Analyzer", theme=gr.themes.Soft()) as
|
|
| 365 |
with gr.Column(scale=1):
|
| 366 |
video_input = gr.Video(label="📹 Upload Video")
|
| 367 |
side_select = gr.Radio(
|
| 368 |
-
choices=["
|
| 369 |
value="right",
|
| 370 |
label="Which side of the cyclist faces the camera?"
|
| 371 |
)
|
|
@@ -377,26 +307,11 @@ with gr.Blocks(title="🚴 AI Bike Fitting Analyzer", theme=gr.themes.Soft()) as
|
|
| 377 |
with gr.Row():
|
| 378 |
plot_output = gr.Image(label="📊 Angle Analysis Over Time")
|
| 379 |
|
| 380 |
-
with gr.Row():
|
| 381 |
-
recommendations_output = gr.Markdown(label="💡 Recommendations")
|
| 382 |
-
|
| 383 |
analyze_btn.click(
|
| 384 |
fn=process_video,
|
| 385 |
inputs=[video_input, side_select],
|
| 386 |
-
outputs=[video_output, plot_output
|
| 387 |
)
|
| 388 |
-
|
| 389 |
-
gr.Markdown("""
|
| 390 |
-
---
|
| 391 |
-
**How it works:** Uses [MediaPipe BlazePose](https://google.github.io/mediapipe/solutions/pose.html)
|
| 392 |
-
to detect 33 body keypoints and calculates joint angles relevant to bike fitting.
|
| 393 |
-
|
| 394 |
-
**Key angles analyzed:**
|
| 395 |
-
- **Torso**: Elbow-shoulder-hip angle (optimal: 80-90°)
|
| 396 |
-
- **Hip**: Shoulder-hip-knee angle (optimal: 38-46°)
|
| 397 |
-
- **Knee**: Hip-knee-ankle angle at bottom of stroke (optimal: 140-148°)
|
| 398 |
-
- **Ankle**: Knee-ankle-foot angle (optimal: 90-100°)
|
| 399 |
-
""")
|
| 400 |
|
| 401 |
|
| 402 |
if __name__ == "__main__":
|
|
|
|
| 10 |
import matplotlib.pyplot as plt
|
| 11 |
import tempfile
|
| 12 |
import os
|
|
|
|
| 13 |
|
| 14 |
# Initialize MediaPipe
|
| 15 |
mp_pose = mp.solutions.pose
|
|
|
|
| 37 |
# Optimal angle ranges for bike fitting
|
| 38 |
OPTIMAL_RANGES = {
|
| 39 |
'torso_angle': (80, 90),
|
| 40 |
+
'hip_angle': (60, 100),
|
| 41 |
+
'knee_angle': (75, 160),
|
| 42 |
+
'ankle_angle': (90, 130),
|
| 43 |
+
'elbow_angle': (150, 175),
|
| 44 |
}
|
| 45 |
|
| 46 |
ANGLE_DESCRIPTIONS = {
|
|
|
|
| 208 |
return plot_path
|
| 209 |
|
| 210 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
def process_video(video_path, side, progress=gr.Progress()):
|
| 212 |
"""Main processing function."""
|
| 213 |
if video_path is None:
|
| 214 |
+
return None, None
|
| 215 |
|
| 216 |
cap = cv2.VideoCapture(video_path)
|
| 217 |
if not cap.isOpened():
|
| 218 |
+
return None, None
|
| 219 |
|
| 220 |
fps = int(cap.get(cv2.CAP_PROP_FPS)) or 30
|
| 221 |
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
|
|
| 231 |
|
| 232 |
with mp_pose.Pose(
|
| 233 |
static_image_mode=False,
|
| 234 |
+
model_complexity=2,
|
| 235 |
min_detection_confidence=0.5,
|
| 236 |
min_tracking_confidence=0.5
|
| 237 |
) as pose:
|
|
|
|
| 270 |
web_output = tempfile.mktemp(suffix='.mp4')
|
| 271 |
os.system(f'ffmpeg -y -i "{output_path}" -vcodec libx264 -acodec aac "{web_output}" -hide_banner -loglevel error')
|
| 272 |
|
| 273 |
+
# Generate plot
|
| 274 |
plot_path = create_plots(angle_history)
|
|
|
|
| 275 |
|
| 276 |
+
return web_output, plot_path
|
| 277 |
|
| 278 |
|
| 279 |
# Build Gradio interface
|
|
|
|
| 295 |
with gr.Column(scale=1):
|
| 296 |
video_input = gr.Video(label="📹 Upload Video")
|
| 297 |
side_select = gr.Radio(
|
| 298 |
+
choices=["left", "right"],
|
| 299 |
value="right",
|
| 300 |
label="Which side of the cyclist faces the camera?"
|
| 301 |
)
|
|
|
|
| 307 |
with gr.Row():
|
| 308 |
plot_output = gr.Image(label="📊 Angle Analysis Over Time")
|
| 309 |
|
|
|
|
|
|
|
|
|
|
| 310 |
analyze_btn.click(
|
| 311 |
fn=process_video,
|
| 312 |
inputs=[video_input, side_select],
|
| 313 |
+
outputs=[video_output, plot_output],
|
| 314 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
|
| 316 |
|
| 317 |
if __name__ == "__main__":
|