chenemii commited on
Commit
bf56017
·
1 Parent(s): 6515800
README.md CHANGED
@@ -1,3 +1,13 @@
 
 
 
 
 
 
 
 
 
 
1
  # Golf Swing Analysis
2
 
3
  A tool for analyzing golf swings using computer vision and AI.
 
1
+ ---
2
+ title: Par-ity Project
3
+ emoji: ⛳
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: streamlit
7
+ app_file: app/streamlit_app.py
8
+ pinned: false
9
+ ---
10
+
11
  # Golf Swing Analysis
12
 
13
  A tool for analyzing golf swings using computer vision and AI.
app/main.py CHANGED
@@ -33,8 +33,8 @@ def main():
33
  "\nEnable GPT analysis? (y/n, default: y): ").lower() != 'n'
34
 
35
  sample_rate_input = input(
36
- "\nFrame skip rate for YOLO (1-10, default: 5, auto-adjusts for videos shorter than 5 seconds): ")
37
- sample_rate = 5 # Default value
38
  if sample_rate_input.isdigit():
39
  sample_rate = max(1, min(10, int(sample_rate_input)))
40
 
 
33
  "\nEnable GPT analysis? (y/n, default: y): ").lower() != 'n'
34
 
35
  sample_rate_input = input(
36
+ "\nFrame processing rate for YOLO (1-10, default: 1 for all frames): ")
37
+ sample_rate = 1 # Default value - process all frames
38
  if sample_rate_input.isdigit():
39
  sample_rate = max(1, min(10, int(sample_rate_input)))
40
 
app/models/llm_analyzer.py CHANGED
@@ -61,15 +61,14 @@ def generate_swing_analysis(pose_data, swing_phases, trajectory_data):
61
  trajectory_data (dict): Dictionary mapping frame indices to trajectory data
62
 
63
  Returns:
64
- str: Detailed swing analysis and coaching tips
65
  """
66
  # Check available services
67
  services = check_llm_services()
68
 
69
- # If no services are available, return sample analysis
70
- if not services['ollama']['available'] and not services['openai'][
71
- 'available']:
72
- return get_sample_analysis()
73
 
74
  # Prepare data for LLM
75
  analysis_data = prepare_data_for_llm(pose_data, swing_phases,
@@ -84,7 +83,7 @@ def generate_swing_analysis(pose_data, swing_phases, trajectory_data):
84
  if analysis:
85
  return analysis
86
  except Exception as e:
87
- print(f"Error with Ollama: {str(e)}. Falling back to OpenAI...")
88
 
89
  # Try OpenAI if available
90
  if services['openai']['available']:
@@ -94,11 +93,10 @@ def generate_swing_analysis(pose_data, swing_phases, trajectory_data):
94
  if analysis:
95
  return analysis
96
  except Exception as e:
97
- print(
98
- f"Error with OpenAI: {str(e)}. Using sample analysis instead.")
99
 
100
- # If both services failed, return sample analysis
101
- return get_sample_analysis()
102
 
103
 
104
  def call_ollama_service(prompt, config):
@@ -164,7 +162,7 @@ def call_openai_service(prompt, config):
164
  try:
165
  # Try with GPT-4 first
166
  response = client.chat.completions.create(
167
- model="gpt-4-turbo",
168
  messages=[{
169
  "role":
170
  "system",
@@ -211,57 +209,6 @@ def call_openai_service(prompt, config):
211
  return None
212
 
213
 
214
- def get_sample_analysis():
215
- """
216
- Return sample analysis when no LLM services are available
217
-
218
- Returns:
219
- str: Sample swing analysis
220
- """
221
- return """
222
- ## Swing Analysis Summary
223
-
224
- Based on the video analysis, here are some observations about your swing:
225
-
226
- ### Setup Phase
227
- - Your stance appears slightly wider than shoulder-width, which can provide good stability
228
- - Your posture shows a good spine angle, though you could bend slightly more from the hips
229
- - The ball position looks appropriate for the club you're using
230
-
231
- ### Backswing
232
- - Your takeaway is smooth with good tempo
233
- - Your wrist hinge develops appropriately in the backswing
234
- - Your right elbow could be kept a bit closer to your body for better consistency
235
-
236
- ### Downswing
237
- - Good weight transfer from back foot to front foot during the transition
238
- - Your hips are rotating well through impact
239
- - The swing plane looks consistent throughout the downswing
240
-
241
- ### Impact
242
- - Club face alignment at impact appears slightly open
243
- - Your head position is stable through impact
244
- - The club path is on a good line toward the target
245
-
246
- ### Follow Through
247
- - Good balance maintained through the finish
248
- - Full extension of arms after impact
249
- - Complete rotation of the body toward the target
250
-
251
- ## Areas for Improvement
252
-
253
- 1. **Club Face Control**: The slightly open club face at impact suggests you may be prone to slicing the ball. Focus on maintaining a square club face through impact.
254
-
255
- 2. **Right Elbow Position**: Keeping your right elbow closer to your body during the backswing will help create a more consistent swing plane.
256
-
257
- 3. **Hip Rotation**: While your hip rotation is good, increasing the speed of rotation could generate more power in your swing.
258
-
259
- 4. **Wrist Release**: Your wrist release could be more active through impact to generate additional club head speed.
260
-
261
- These adjustments should help improve both consistency and distance in your swing.
262
- """
263
-
264
-
265
  def prepare_data_for_llm(pose_data, swing_phases, trajectory_data):
266
  """
267
  Prepare swing data for LLM analysis
@@ -390,12 +337,6 @@ I've analyzed a golf swing and extracted the following data:
390
  for phase, data in analysis_data["swing_phases"].items():
391
  prompt += f"- {phase.capitalize()}: Frame {data['frame_index']}, Duration: {data['duration_frames']} frames\n"
392
 
393
- # Add trajectory information
394
- prompt += "\n## Trajectory Data\n"
395
- if "trajectory" in analysis_data and "club_speed_mph" in analysis_data[
396
- "trajectory"]:
397
- prompt += f"- Club Speed: {analysis_data['trajectory']['club_speed_mph']:.1f} mph\n"
398
-
399
  # Add detailed biomechanical metrics
400
  prompt += "\n## Swing Mechanics\n"
401
 
@@ -440,20 +381,6 @@ I've analyzed a golf swing and extracted the following data:
440
  int(analysis_data["metrics"].get("ground_force_efficiency", 0.7) *
441
  100))
442
 
443
- # Swing path and clubface metrics
444
- prompt += "\n### Club Path & Face Metrics\n"
445
- prompt += "- Swing Path (degrees): {} ({})\n".format(
446
- analysis_data["metrics"].get("swing_path", 2.5), "Out-to-In"
447
- if analysis_data["metrics"].get("swing_path", 0) > 0 else "In-to-Out")
448
- prompt += "- Clubface Angle (degrees): {} ({})\n".format(
449
- analysis_data["metrics"].get("clubface_angle", 2.1), "Open"
450
- if analysis_data["metrics"].get("clubface_angle", 0) > 0 else "Closed")
451
- prompt += "- Attack Angle (degrees): {} ({})\n".format(
452
- analysis_data["metrics"].get("attack_angle", -4.2), "Descending" if
453
- analysis_data["metrics"].get("attack_angle", 0) < 0 else "Ascending")
454
- prompt += "- Club Path Consistency: {}%\n".format(
455
- int(analysis_data["metrics"].get("club_path_consistency", 0.78) * 100))
456
-
457
  # Tempo and timing metrics
458
  prompt += "\n### Tempo & Timing\n"
459
  prompt += "- Transition Smoothness: {}%\n".format(
@@ -492,21 +419,6 @@ Based on this detailed biomechanical data, please provide:
492
  - Physical limitations
493
  - Technical flaws
494
 
495
- 3. Prioritized recommendations for improvement:
496
- - Top 3-5 most impactful changes to make
497
- - Root cause analysis (why these issues are occurring)
498
- - Expected improvement in performance from each change
499
-
500
- 4. Specific drills and exercises addressing each issue:
501
- - Technical drills for swing mechanics
502
- - Physical exercises to address any biomechanical limitations
503
- - Feel-based drills to develop proper movement patterns
504
- - Practice routine recommendations
505
-
506
- 5. Long-term development plan:
507
- - Sequential order of what to work on
508
- - Benchmarks for measuring progress
509
- - Timeline for improvement
510
 
511
  Please be specific, detailed, and actionable in your feedback, providing the kind of analysis a professional golf coach would give after a thorough assessment.
512
  """
 
61
  trajectory_data (dict): Dictionary mapping frame indices to trajectory data
62
 
63
  Returns:
64
+ str: Detailed swing analysis and coaching tips, or error message
65
  """
66
  # Check available services
67
  services = check_llm_services()
68
 
69
+ # If no services are available, return error message
70
+ if not services['ollama']['available'] and not services['openai']['available']:
71
+ return "Error: No AI services available. Please ensure either Ollama is running or OpenAI API key is configured."
 
72
 
73
  # Prepare data for LLM
74
  analysis_data = prepare_data_for_llm(pose_data, swing_phases,
 
83
  if analysis:
84
  return analysis
85
  except Exception as e:
86
+ print(f"Error with Ollama: {str(e)}")
87
 
88
  # Try OpenAI if available
89
  if services['openai']['available']:
 
93
  if analysis:
94
  return analysis
95
  except Exception as e:
96
+ print(f"Error with OpenAI: {str(e)}")
 
97
 
98
+ # If both services failed, return error message
99
+ return "Error: All AI services failed. Please check your API keys and service configurations."
100
 
101
 
102
  def call_ollama_service(prompt, config):
 
162
  try:
163
  # Try with GPT-4 first
164
  response = client.chat.completions.create(
165
+ model="gpt-4o-mini",
166
  messages=[{
167
  "role":
168
  "system",
 
209
  return None
210
 
211
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  def prepare_data_for_llm(pose_data, swing_phases, trajectory_data):
213
  """
214
  Prepare swing data for LLM analysis
 
337
  for phase, data in analysis_data["swing_phases"].items():
338
  prompt += f"- {phase.capitalize()}: Frame {data['frame_index']}, Duration: {data['duration_frames']} frames\n"
339
 
 
 
 
 
 
 
340
  # Add detailed biomechanical metrics
341
  prompt += "\n## Swing Mechanics\n"
342
 
 
381
  int(analysis_data["metrics"].get("ground_force_efficiency", 0.7) *
382
  100))
383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  # Tempo and timing metrics
385
  prompt += "\n### Tempo & Timing\n"
386
  prompt += "- Transition Smoothness: {}%\n".format(
 
419
  - Physical limitations
420
  - Technical flaws
421
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
 
423
  Please be specific, detailed, and actionable in your feedback, providing the kind of analysis a professional golf coach would give after a thorough assessment.
424
  """
app/models/swing_analyzer.py CHANGED
@@ -23,7 +23,7 @@ def find_top_of_backswing(pose_data):
23
  return top_frame
24
 
25
 
26
- def detect_impact_frame(pose_data, detections, sample_rate=2):
27
  """
28
  Simple impact detection: ball movement first, wrist speed fallback
29
  """
@@ -42,12 +42,29 @@ def detect_impact_frame(pose_data, detections, sample_rate=2):
42
  ball_detections = [d for d in detections if d.class_name == "sports ball"]
43
  ball_positions = {}
44
 
 
 
 
 
 
 
45
  for detection in ball_detections:
46
- frame_idx = detection.frame_idx // sample_rate
47
- if frame_idx > top_backswing:
 
 
 
 
 
 
 
 
 
 
 
48
  x1, y1, x2, y2 = detection.bbox
49
  center_x, center_y = (x1 + x2) / 2, (y1 + y2) / 2
50
- ball_positions[frame_idx] = (center_x, center_y)
51
 
52
  # Find first significant ball movement
53
  if len(ball_positions) >= 2:
@@ -58,7 +75,7 @@ def detect_impact_frame(pose_data, detections, sample_rate=2):
58
  movement = np.sqrt((curr_pos[0] - prev_pos[0])**2 + (curr_pos[1] - prev_pos[1])**2)
59
 
60
  if movement > 15: # Significant movement threshold
61
- print(f"Impact detected via ball movement at frame {sorted_frames[i]}")
62
  return sorted_frames[i]
63
 
64
  # Method 2: Wrist speed fallback (simple and reliable)
@@ -80,11 +97,11 @@ def detect_impact_frame(pose_data, detections, sample_rate=2):
80
  max_wrist_speed = wrist_speed
81
  impact_frame = curr_frame
82
 
83
- print(f"Impact detected via wrist speed at frame {impact_frame}")
84
  return impact_frame or downswing_frames[len(downswing_frames) // 3]
85
 
86
 
87
- def segment_swing_pose_based(pose_data, detections=None, sample_rate=2):
88
  """
89
  Simple swing segmentation with clean impact detection
90
  """
@@ -117,7 +134,7 @@ def segment_swing_pose_based(pose_data, detections=None, sample_rate=2):
117
  downswing_frames = [f for f in frame_indices if f > top_backswing]
118
  impact_frame = downswing_frames[len(downswing_frames) // 3] if downswing_frames else top_backswing + 1
119
 
120
- print(f"Swing phases: Setup end={setup_end}, Top backswing={top_backswing}, Impact={impact_frame}")
121
 
122
  # 4. Assign phases
123
  for idx in frame_indices:
@@ -136,14 +153,14 @@ def segment_swing_pose_based(pose_data, detections=None, sample_rate=2):
136
 
137
 
138
  # Wrapper function to maintain compatibility with existing Streamlit app
139
- def segment_swing(pose_data, detections, sample_rate=2):
140
  """
141
  Main swing segmentation function (wrapper for pose-based approach)
142
  """
143
  return segment_swing_pose_based(pose_data, detections, sample_rate)
144
 
145
 
146
- def analyze_trajectory(frames, detections, swing_phases, sample_rate=2):
147
  """
148
  Analyze ball trajectory and calculate club speed
149
  """
 
23
  return top_frame
24
 
25
 
26
+ def detect_impact_frame(pose_data, detections, sample_rate=1):
27
  """
28
  Simple impact detection: ball movement first, wrist speed fallback
29
  """
 
42
  ball_detections = [d for d in detections if d.class_name == "sports ball"]
43
  ball_positions = {}
44
 
45
+ # Create a mapping from original video frame indices to processed frame indices
46
+ original_to_processed = {}
47
+ for processed_idx in frame_indices:
48
+ original_frame_idx = processed_idx * sample_rate
49
+ original_to_processed[original_frame_idx] = processed_idx
50
+
51
  for detection in ball_detections:
52
+ original_frame_idx = detection.frame_idx
53
+ # Find the closest processed frame index
54
+ processed_frame_idx = None
55
+ if original_frame_idx in original_to_processed:
56
+ processed_frame_idx = original_to_processed[original_frame_idx]
57
+ else:
58
+ # Find closest processed frame
59
+ closest_original = min(original_to_processed.keys(),
60
+ key=lambda x: abs(x - original_frame_idx))
61
+ if abs(closest_original - original_frame_idx) <= sample_rate:
62
+ processed_frame_idx = original_to_processed[closest_original]
63
+
64
+ if processed_frame_idx and processed_frame_idx > top_backswing:
65
  x1, y1, x2, y2 = detection.bbox
66
  center_x, center_y = (x1 + x2) / 2, (y1 + y2) / 2
67
+ ball_positions[processed_frame_idx] = (center_x, center_y)
68
 
69
  # Find first significant ball movement
70
  if len(ball_positions) >= 2:
 
75
  movement = np.sqrt((curr_pos[0] - prev_pos[0])**2 + (curr_pos[1] - prev_pos[1])**2)
76
 
77
  if movement > 15: # Significant movement threshold
78
+ print(f"Impact detected via ball movement at processed frame {sorted_frames[i]} (original frame {sorted_frames[i] * sample_rate})")
79
  return sorted_frames[i]
80
 
81
  # Method 2: Wrist speed fallback (simple and reliable)
 
97
  max_wrist_speed = wrist_speed
98
  impact_frame = curr_frame
99
 
100
+ print(f"Impact detected via wrist speed at processed frame {impact_frame} (original frame {impact_frame * sample_rate if impact_frame else 'N/A'})")
101
  return impact_frame or downswing_frames[len(downswing_frames) // 3]
102
 
103
 
104
+ def segment_swing_pose_based(pose_data, detections=None, sample_rate=1):
105
  """
106
  Simple swing segmentation with clean impact detection
107
  """
 
134
  downswing_frames = [f for f in frame_indices if f > top_backswing]
135
  impact_frame = downswing_frames[len(downswing_frames) // 3] if downswing_frames else top_backswing + 1
136
 
137
+ print(f"Swing phases: Setup end={setup_end} (orig {setup_end * sample_rate}), Top backswing={top_backswing} (orig {top_backswing * sample_rate}), Impact={impact_frame} (orig {impact_frame * sample_rate if impact_frame else 'N/A'})")
138
 
139
  # 4. Assign phases
140
  for idx in frame_indices:
 
153
 
154
 
155
  # Wrapper function to maintain compatibility with existing Streamlit app
156
+ def segment_swing(pose_data, detections, sample_rate=1):
157
  """
158
  Main swing segmentation function (wrapper for pose-based approach)
159
  """
160
  return segment_swing_pose_based(pose_data, detections, sample_rate)
161
 
162
 
163
+ def analyze_trajectory(frames, detections, swing_phases, sample_rate=1):
164
  """
165
  Analyze ball trajectory and calculate club speed
166
  """
app/streamlit_app.py CHANGED
@@ -161,14 +161,14 @@ def main():
161
  else:
162
  st.sidebar.info("Using sample analysis mode (no LLM required)")
163
 
164
- # Frame skip rate for YOLO
165
  sample_rate = st.sidebar.slider(
166
- "Frame Skip Rate (YOLO)",
167
  min_value=1,
168
  max_value=10,
169
- value=2,
170
  help=
171
- "Process every Nth frame. Higher values = faster but less accurate.")
172
 
173
  # Pro reference toggle
174
  enable_pro_comparison = st.sidebar.checkbox(
 
161
  else:
162
  st.sidebar.info("Using sample analysis mode (no LLM required)")
163
 
164
+ # Frame processing rate for YOLO
165
  sample_rate = st.sidebar.slider(
166
+ "Frame Processing Rate (YOLO)",
167
  min_value=1,
168
  max_value=10,
169
+ value=1,
170
  help=
171
+ "Process every Nth frame. 1 = all frames (most accurate), higher values = faster but less accurate.")
172
 
173
  # Pro reference toggle
174
  enable_pro_comparison = st.sidebar.checkbox(
app/utils/comparison.py CHANGED
@@ -162,6 +162,7 @@ def extract_key_swing_frames(video_path, frames, swing_phases=None):
162
  impact_idx = len(frames) // 2
163
 
164
  print(f"Key frame indices (relative to processed frames) - Setup: {setup_idx}, Backswing: {backswing_idx}, Impact: {impact_idx}")
 
165
 
166
  # Get rotation angle from the original video file
167
  rotation_angle = 0
 
162
  impact_idx = len(frames) // 2
163
 
164
  print(f"Key frame indices (relative to processed frames) - Setup: {setup_idx}, Backswing: {backswing_idx}, Impact: {impact_idx}")
165
+ print(f"These correspond to original video frames (approx) - Setup: ~{setup_idx * 1}, Backswing: ~{backswing_idx * 1}, Impact: ~{impact_idx * 1} (assuming sample_rate=1)")
166
 
167
  # Get rotation angle from the original video file
168
  rotation_angle = 0
app/utils/video_processor.py CHANGED
@@ -20,13 +20,13 @@ class Detection:
20
  self.confidence = confidence
21
 
22
 
23
- def process_video(video_path, sample_rate=5):
24
  """
25
  Process video and detect golfer, club, and ball
26
 
27
  Args:
28
  video_path (str): Path to the video file
29
- sample_rate (int): Process every nth frame
30
 
31
  Returns:
32
  tuple: (frames, detections)
@@ -50,10 +50,7 @@ def process_video(video_path, sample_rate=5):
50
 
51
  frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
52
 
53
- if frame_count < 150:
54
- print(f"Short video detected ({frame_count} frames). Processing all frames.")
55
- sample_rate = 1
56
-
57
  frames = []
58
  detections = []
59
 
 
20
  self.confidence = confidence
21
 
22
 
23
+ def process_video(video_path, sample_rate=1):
24
  """
25
  Process video and detect golfer, club, and ball
26
 
27
  Args:
28
  video_path (str): Path to the video file
29
+ sample_rate (int): Process every nth frame (default: 1 for all frames)
30
 
31
  Returns:
32
  tuple: (frames, detections)
 
50
 
51
  frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
52
 
53
+ # Process all frames by default
 
 
 
54
  frames = []
55
  detections = []
56
 
app/utils/visualizer.py CHANGED
@@ -38,7 +38,7 @@ def create_annotated_video(video_path,
38
  swing_phases,
39
  trajectory_data,
40
  output_dir="downloads",
41
- sample_rate=5):
42
  """
43
  Create an annotated video with swing analysis visualizations
44
 
 
38
  swing_phases,
39
  trajectory_data,
40
  output_dir="downloads",
41
+ sample_rate=1):
42
  """
43
  Create an annotated video with swing analysis visualizations
44