GerardCB commited on
Commit
ed91e86
·
verified ·
1 Parent(s): d5740b2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +11 -96
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': (38, 46),
42
- 'knee_angle': (140, 148),
43
- 'ankle_angle': (90, 100),
44
- 'elbow_angle': (150, 170),
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, "Please upload a video."
284
 
285
  cap = cv2.VideoCapture(video_path)
286
  if not cap.isOpened():
287
- return None, None, "Could not open video file."
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=1,
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 outputs
343
  plot_path = create_plots(angle_history)
344
- recommendations = generate_recommendations(angle_history)
345
 
346
- return web_output, plot_path, recommendations
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=["right", "left"],
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, recommendations_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__":