Spaces:
Paused
Paused
comparison photo
Browse files- README.md +8 -4
- app/streamlit_app.py +79 -32
- app/utils/comparison.py +296 -0
- app/utils/visualizer.py +3 -6
README.md
CHANGED
|
@@ -11,8 +11,8 @@ A tool for analyzing golf swings using computer vision and AI.
|
|
| 11 |
- Club and ball trajectory analysis
|
| 12 |
- LLM-powered swing analysis and coaching tips (OpenAI GPT-4/3.5 or local Ollama models)
|
| 13 |
- Annotated video generation
|
| 14 |
-
-
|
| 15 |
-
-
|
| 16 |
|
| 17 |
## Setup
|
| 18 |
|
|
@@ -74,8 +74,12 @@ streamlit run app/streamlit_app.py
|
|
| 74 |
2. Click "Analyze Swing" to process the video
|
| 75 |
3. View the swing phase breakdown and metrics
|
| 76 |
4. Generate an annotated video showing the analysis
|
| 77 |
-
5. Compare your swing
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
## Technical Details
|
| 81 |
|
|
|
|
| 11 |
- Club and ball trajectory analysis
|
| 12 |
- LLM-powered swing analysis and coaching tips (OpenAI GPT-4/3.5 or local Ollama models)
|
| 13 |
- Annotated video generation
|
| 14 |
+
- Key position comparison with professional golfer (3 critical swing positions)
|
| 15 |
+
- Detailed improvement recommendations with visual analysis
|
| 16 |
|
| 17 |
## Setup
|
| 18 |
|
|
|
|
| 74 |
2. Click "Analyze Swing" to process the video
|
| 75 |
3. View the swing phase breakdown and metrics
|
| 76 |
4. Generate an annotated video showing the analysis
|
| 77 |
+
5. Compare your swing at 3 key positions with a professional golfer:
|
| 78 |
+
- Starting position (setup)
|
| 79 |
+
- Top of backswing
|
| 80 |
+
- Impact with ball
|
| 81 |
+
6. Get detailed improvement recommendations for each swing phase
|
| 82 |
+
7. Download comparison images and analysis results
|
| 83 |
|
| 84 |
## Technical Details
|
| 85 |
|
app/streamlit_app.py
CHANGED
|
@@ -23,7 +23,7 @@ from app.models.pose_estimator import analyze_pose
|
|
| 23 |
from app.models.swing_analyzer import segment_swing, analyze_trajectory
|
| 24 |
from app.models.llm_analyzer import generate_swing_analysis, create_llm_prompt, prepare_data_for_llm, check_llm_services
|
| 25 |
from app.utils.visualizer import create_annotated_video
|
| 26 |
-
from app.utils.comparison import
|
| 27 |
|
| 28 |
# Set page config
|
| 29 |
st.set_page_config(page_title="Par-ity Project: Golf Swing Analysis 🏌️♀️",
|
|
@@ -304,7 +304,7 @@ def main():
|
|
| 304 |
with options_col3:
|
| 305 |
if enable_pro_comparison and st.session_state.pro_reference_path:
|
| 306 |
st.info(
|
| 307 |
-
"**Option 3: Compare With Pro**\n\nSee
|
| 308 |
)
|
| 309 |
|
| 310 |
except Exception as e:
|
|
@@ -365,7 +365,7 @@ def main():
|
|
| 365 |
# Add pro comparison button if enabled
|
| 366 |
if enable_pro_comparison and st.session_state.pro_reference_path and button_col3:
|
| 367 |
with button_col3:
|
| 368 |
-
comparison_clicked = st.button("Compare
|
| 369 |
key="pro_comparison",
|
| 370 |
use_container_width=True)
|
| 371 |
else:
|
|
@@ -480,41 +480,91 @@ def main():
|
|
| 480 |
# Handle pro comparison video creation
|
| 481 |
if comparison_clicked and st.session_state.pro_reference_path:
|
| 482 |
try:
|
| 483 |
-
with st.spinner("Creating frame
|
| 484 |
# Get data from session state
|
| 485 |
user_video_path = st.session_state.analysis_data['video_path']
|
| 486 |
-
|
| 487 |
|
| 488 |
-
# Create the comparison
|
| 489 |
-
|
|
|
|
| 490 |
user_video_path,
|
| 491 |
-
|
|
|
|
| 492 |
)
|
| 493 |
|
| 494 |
-
#
|
| 495 |
-
|
| 496 |
-
raise FileNotFoundError(
|
| 497 |
-
f"Comparison video file not found at {comparison_path}")
|
| 498 |
-
|
| 499 |
-
# Store the comparison video path in session state
|
| 500 |
-
st.session_state.comparison_video_path = comparison_path
|
| 501 |
|
| 502 |
-
# Display success message
|
| 503 |
-
st.success("
|
| 504 |
-
st.subheader("
|
| 505 |
|
| 506 |
-
# Display
|
| 507 |
-
|
| 508 |
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
data
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
|
| 519 |
# Add some guidance for interpreting the comparison
|
| 520 |
with st.expander("How to use this comparison", expanded=True):
|
|
@@ -532,9 +582,6 @@ def main():
|
|
| 532 |
|
| 533 |
Try pausing the video at key positions to analyze differences in detail.
|
| 534 |
""")
|
| 535 |
-
|
| 536 |
-
except Exception as e:
|
| 537 |
-
st.error(f"Error creating comparison video: {str(e)}")
|
| 538 |
|
| 539 |
|
| 540 |
if __name__ == "__main__":
|
|
|
|
| 23 |
from app.models.swing_analyzer import segment_swing, analyze_trajectory
|
| 24 |
from app.models.llm_analyzer import generate_swing_analysis, create_llm_prompt, prepare_data_for_llm, check_llm_services
|
| 25 |
from app.utils.visualizer import create_annotated_video
|
| 26 |
+
from app.utils.comparison import create_key_frame_comparison
|
| 27 |
|
| 28 |
# Set page config
|
| 29 |
st.set_page_config(page_title="Par-ity Project: Golf Swing Analysis 🏌️♀️",
|
|
|
|
| 304 |
with options_col3:
|
| 305 |
if enable_pro_comparison and st.session_state.pro_reference_path:
|
| 306 |
st.info(
|
| 307 |
+
"**Option 3: Compare With Pro**\n\nSee side-by-side comparisons of 3 key swing positions with a professional golfer, including improvement tips for each phase."
|
| 308 |
)
|
| 309 |
|
| 310 |
except Exception as e:
|
|
|
|
| 365 |
# Add pro comparison button if enabled
|
| 366 |
if enable_pro_comparison and st.session_state.pro_reference_path and button_col3:
|
| 367 |
with button_col3:
|
| 368 |
+
comparison_clicked = st.button("Compare Key Positions",
|
| 369 |
key="pro_comparison",
|
| 370 |
use_container_width=True)
|
| 371 |
else:
|
|
|
|
| 480 |
# Handle pro comparison video creation
|
| 481 |
if comparison_clicked and st.session_state.pro_reference_path:
|
| 482 |
try:
|
| 483 |
+
with st.spinner("Creating key frame comparison..."):
|
| 484 |
# Get data from session state
|
| 485 |
user_video_path = st.session_state.analysis_data['video_path']
|
| 486 |
+
user_swing_phases = st.session_state.analysis_data['swing_phases']
|
| 487 |
|
| 488 |
+
# Create the key frame comparison using static pro reference images
|
| 489 |
+
# Don't pass pro_video_path to ensure it uses the static images
|
| 490 |
+
comparison_data = create_key_frame_comparison(
|
| 491 |
user_video_path,
|
| 492 |
+
user_swing_phases=user_swing_phases,
|
| 493 |
+
use_pro_images=True
|
| 494 |
)
|
| 495 |
|
| 496 |
+
# Store the comparison data in session state
|
| 497 |
+
st.session_state.comparison_data = comparison_data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
|
| 499 |
+
# Display success message
|
| 500 |
+
st.success("Key frame comparison created successfully!")
|
| 501 |
+
st.subheader("Swing Analysis: Key Position Comparison")
|
| 502 |
|
| 503 |
+
# Display each comparison with comments
|
| 504 |
+
phases = ['setup', 'backswing', 'impact']
|
| 505 |
|
| 506 |
+
for phase in phases:
|
| 507 |
+
if phase in comparison_data:
|
| 508 |
+
data = comparison_data[phase]
|
| 509 |
+
|
| 510 |
+
# Display the comparison image
|
| 511 |
+
st.subheader(f"{data['title']}")
|
| 512 |
+
|
| 513 |
+
# Display the image
|
| 514 |
+
if os.path.exists(data['image_path']):
|
| 515 |
+
st.image(data['image_path'], use_column_width=True)
|
| 516 |
+
|
| 517 |
+
# Create download button for the image
|
| 518 |
+
with open(data['image_path'], "rb") as file:
|
| 519 |
+
image_bytes = file.read()
|
| 520 |
+
st.download_button(
|
| 521 |
+
label=f"Download {data['title']} Comparison",
|
| 522 |
+
data=image_bytes,
|
| 523 |
+
file_name=os.path.basename(data['image_path']),
|
| 524 |
+
mime="image/jpeg",
|
| 525 |
+
key=f"download_{phase}"
|
| 526 |
+
)
|
| 527 |
+
|
| 528 |
+
# Display improvement comments
|
| 529 |
+
comments = data['comments']
|
| 530 |
+
|
| 531 |
+
col1, col2 = st.columns(2)
|
| 532 |
+
|
| 533 |
+
with col1:
|
| 534 |
+
st.markdown("**🏆 Professional Analysis:**")
|
| 535 |
+
for analysis in comments['pro_analysis']:
|
| 536 |
+
st.markdown(f"• {analysis}")
|
| 537 |
+
|
| 538 |
+
with col2:
|
| 539 |
+
st.markdown("**🔄 User vs Professional Comparison:**")
|
| 540 |
+
for comparison in comments['comparison']:
|
| 541 |
+
st.markdown(f"• {comparison}")
|
| 542 |
+
|
| 543 |
+
st.markdown("---") # Add separator between phases
|
| 544 |
+
|
| 545 |
+
# Add general guidance
|
| 546 |
+
with st.expander("How to Use This Analysis", expanded=False):
|
| 547 |
+
st.markdown("""
|
| 548 |
+
### How to Interpret These Comparisons
|
| 549 |
+
|
| 550 |
+
Each comparison shows your swing position (left) next to a professional golfer's position (right) at three critical moments:
|
| 551 |
+
|
| 552 |
+
1. **Starting Position**: Your setup and address position
|
| 553 |
+
2. **Top of Backswing**: The highest point of your backswing
|
| 554 |
+
3. **Impact with Ball**: The moment of contact with the ball
|
| 555 |
+
|
| 556 |
+
**Tips for Improvement:**
|
| 557 |
+
- Compare your body positioning, posture, and club position to the pro
|
| 558 |
+
- Focus on one aspect at a time (e.g., posture, then weight distribution)
|
| 559 |
+
- Practice the positions slowly without a ball first
|
| 560 |
+
- Use a mirror or video recording to check your positions
|
| 561 |
+
- Work with a golf instructor for personalized feedback
|
| 562 |
+
|
| 563 |
+
**Remember:** Every golfer is different, so focus on the fundamental principles rather than trying to copy every detail exactly.
|
| 564 |
+
""")
|
| 565 |
+
|
| 566 |
+
except Exception as e:
|
| 567 |
+
st.error(f"Error creating key frame comparison: {str(e)}")
|
| 568 |
|
| 569 |
# Add some guidance for interpreting the comparison
|
| 570 |
with st.expander("How to use this comparison", expanded=True):
|
|
|
|
| 582 |
|
| 583 |
Try pausing the video at key positions to analyze differences in detail.
|
| 584 |
""")
|
|
|
|
|
|
|
|
|
|
| 585 |
|
| 586 |
|
| 587 |
if __name__ == "__main__":
|
app/utils/comparison.py
CHANGED
|
@@ -52,6 +52,302 @@ def extract_frames(video_path, max_frames=100):
|
|
| 52 |
return frames
|
| 53 |
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
def normalize_frames(frames, target_height=480):
|
| 56 |
"""
|
| 57 |
Normalize frames to a consistent size while maintaining aspect ratio
|
|
|
|
| 52 |
return frames
|
| 53 |
|
| 54 |
|
| 55 |
+
def extract_key_swing_frames(video_path, swing_phases=None):
|
| 56 |
+
"""
|
| 57 |
+
Extract 3 key frames from a golf swing video:
|
| 58 |
+
1. Starting position (setup)
|
| 59 |
+
2. Top of backswing
|
| 60 |
+
3. Impact with ball
|
| 61 |
+
|
| 62 |
+
Args:
|
| 63 |
+
video_path (str): Path to the video file
|
| 64 |
+
swing_phases (dict): Optional swing phase data for precise frame selection
|
| 65 |
+
|
| 66 |
+
Returns:
|
| 67 |
+
dict: Dictionary with keys 'setup', 'backswing', 'impact'
|
| 68 |
+
and frame images as values
|
| 69 |
+
"""
|
| 70 |
+
if not os.path.exists(video_path):
|
| 71 |
+
raise ValueError(f"Video file not found: {video_path}")
|
| 72 |
+
|
| 73 |
+
cap = cv2.VideoCapture(video_path)
|
| 74 |
+
|
| 75 |
+
if not cap.isOpened():
|
| 76 |
+
raise ValueError(f"Could not open video: {video_path}")
|
| 77 |
+
|
| 78 |
+
# Get total frame count
|
| 79 |
+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 80 |
+
|
| 81 |
+
key_frames = {}
|
| 82 |
+
|
| 83 |
+
if swing_phases:
|
| 84 |
+
# Use provided swing phase data for precise frame selection
|
| 85 |
+
frame_indices = {
|
| 86 |
+
'setup': swing_phases.get('setup', [0])[0] if swing_phases.get('setup') else 0,
|
| 87 |
+
'backswing': swing_phases.get('backswing', [total_frames//3])[-1] if swing_phases.get('backswing') else total_frames//3,
|
| 88 |
+
'impact': swing_phases.get('impact', [total_frames//2])[len(swing_phases.get('impact', [total_frames//2]))//2] if swing_phases.get('impact') else total_frames//2
|
| 89 |
+
}
|
| 90 |
+
else:
|
| 91 |
+
# Use estimated frame positions for 3 frames
|
| 92 |
+
frame_indices = {
|
| 93 |
+
'setup': 0, # First frame
|
| 94 |
+
'backswing': total_frames // 3, # 33% through
|
| 95 |
+
'impact': int(total_frames * 0.6) # 60% through
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
# Extract the specific frames
|
| 99 |
+
for phase, frame_idx in frame_indices.items():
|
| 100 |
+
cap.set(cv2.CAP_PROP_POS_FRAMES, min(frame_idx, total_frames - 1))
|
| 101 |
+
ret, frame = cap.read()
|
| 102 |
+
if ret:
|
| 103 |
+
# Keep original orientation - no rotation
|
| 104 |
+
key_frames[phase] = frame
|
| 105 |
+
else:
|
| 106 |
+
# If frame extraction fails, use a black frame
|
| 107 |
+
key_frames[phase] = np.zeros((480, 640, 3), dtype=np.uint8)
|
| 108 |
+
|
| 109 |
+
cap.release()
|
| 110 |
+
|
| 111 |
+
return key_frames
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def generate_improvement_comments(phase):
|
| 115 |
+
"""
|
| 116 |
+
Generate improvement comments for each swing phase in Professional/Comparison format
|
| 117 |
+
|
| 118 |
+
Args:
|
| 119 |
+
phase (str): The swing phase ('setup', 'backswing', 'impact')
|
| 120 |
+
|
| 121 |
+
Returns:
|
| 122 |
+
dict: Dictionary with 'pro_analysis' and 'comparison' keys
|
| 123 |
+
"""
|
| 124 |
+
comments = {
|
| 125 |
+
'setup': {
|
| 126 |
+
'pro_analysis': [
|
| 127 |
+
"Balanced stance with feet shoulder-width apart",
|
| 128 |
+
"Even weight distribution on both feet",
|
| 129 |
+
"Neutral grip with hands in proper position",
|
| 130 |
+
"Athletic posture with slight forward bend",
|
| 131 |
+
"Ball positioned correctly for club selection"
|
| 132 |
+
],
|
| 133 |
+
'comparison': [
|
| 134 |
+
"Compare your stance width to the pro's balanced setup",
|
| 135 |
+
"Check if your weight is evenly distributed like the pro",
|
| 136 |
+
"Ensure your grip matches the pro's neutral hand position",
|
| 137 |
+
"Adjust your posture to match the pro's athletic stance",
|
| 138 |
+
"Position the ball in your stance similar to the pro"
|
| 139 |
+
]
|
| 140 |
+
},
|
| 141 |
+
'backswing': {
|
| 142 |
+
'pro_analysis': [
|
| 143 |
+
"Full 90+ degree shoulder rotation",
|
| 144 |
+
"Controlled hip turn with stable lower body",
|
| 145 |
+
"Club on proper swing plane at top",
|
| 146 |
+
"Consistent spine angle throughout",
|
| 147 |
+
"Minimal weight shift to right side"
|
| 148 |
+
],
|
| 149 |
+
'comparison': [
|
| 150 |
+
"Increase your shoulder turn to match the pro's full rotation",
|
| 151 |
+
"Control your hip movement like the pro's stable base",
|
| 152 |
+
"Adjust your club position to match the pro's swing plane",
|
| 153 |
+
"Maintain spine angle consistency like the professional",
|
| 154 |
+
"Minimize weight shift compared to the pro's centered position"
|
| 155 |
+
]
|
| 156 |
+
},
|
| 157 |
+
'impact': {
|
| 158 |
+
'pro_analysis': [
|
| 159 |
+
"Weight shifted to front foot (70-80%)",
|
| 160 |
+
"Hands ahead of ball at impact",
|
| 161 |
+
"Square club face to target line",
|
| 162 |
+
"Head behind ball with steady position",
|
| 163 |
+
"Hips and shoulders aligned to target"
|
| 164 |
+
],
|
| 165 |
+
'comparison': [
|
| 166 |
+
"Shift more weight to your front foot like the pro",
|
| 167 |
+
"Get your hands ahead of the ball like the professional",
|
| 168 |
+
"Square your club face to match the pro's alignment",
|
| 169 |
+
"Keep your head steady and behind the ball like the pro",
|
| 170 |
+
"Align your body to the target like the professional"
|
| 171 |
+
]
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
return comments.get(phase, {'pro_analysis': [], 'comparison': []})
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def load_pro_reference_images(pro_images_dir="pro_reference"):
|
| 179 |
+
"""
|
| 180 |
+
Load professional golfer reference images from directory
|
| 181 |
+
|
| 182 |
+
Args:
|
| 183 |
+
pro_images_dir (str): Directory containing professional reference images
|
| 184 |
+
|
| 185 |
+
Returns:
|
| 186 |
+
dict: Dictionary with phase names as keys and image arrays as values
|
| 187 |
+
"""
|
| 188 |
+
# Get the absolute path to the pro_reference directory
|
| 189 |
+
# This ensures it works regardless of the current working directory
|
| 190 |
+
if not os.path.isabs(pro_images_dir):
|
| 191 |
+
# Get the directory where this script is located
|
| 192 |
+
script_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 193 |
+
pro_images_dir = os.path.join(script_dir, pro_images_dir)
|
| 194 |
+
|
| 195 |
+
pro_frames = {}
|
| 196 |
+
|
| 197 |
+
# Expected filenames for the 3 phases
|
| 198 |
+
phase_files = {
|
| 199 |
+
'setup': 'setup.jpg',
|
| 200 |
+
'backswing': 'backswing.jpg',
|
| 201 |
+
'impact': 'impact.jpg'
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
for phase, filename in phase_files.items():
|
| 205 |
+
image_path = os.path.join(pro_images_dir, filename)
|
| 206 |
+
if os.path.exists(image_path):
|
| 207 |
+
image = cv2.imread(image_path)
|
| 208 |
+
if image is not None:
|
| 209 |
+
pro_frames[phase] = image
|
| 210 |
+
else:
|
| 211 |
+
# Create a placeholder if image can't be loaded
|
| 212 |
+
pro_frames[phase] = np.zeros((480, 640, 3), dtype=np.uint8)
|
| 213 |
+
else:
|
| 214 |
+
# Create a placeholder if file doesn't exist
|
| 215 |
+
pro_frames[phase] = np.zeros((480, 640, 3), dtype=np.uint8)
|
| 216 |
+
|
| 217 |
+
return pro_frames
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def create_key_frame_comparison(user_video_path, pro_video_path=None, user_swing_phases=None, pro_swing_phases=None, output_dir="downloads", use_pro_images=True):
|
| 221 |
+
"""
|
| 222 |
+
Create a comparison of 3 key frames between user and pro golfer swings
|
| 223 |
+
|
| 224 |
+
Args:
|
| 225 |
+
user_video_path (str): Path to the user's golf swing video
|
| 226 |
+
pro_video_path (str): Path to the professional golfer's swing video (optional if use_pro_images=True)
|
| 227 |
+
user_swing_phases (dict): Optional swing phase data for user video
|
| 228 |
+
pro_swing_phases (dict): Optional swing phase data for pro video
|
| 229 |
+
output_dir (str): Directory to save the comparison images
|
| 230 |
+
use_pro_images (bool): Whether to use provided pro reference images instead of video
|
| 231 |
+
|
| 232 |
+
Returns:
|
| 233 |
+
dict: Dictionary with phase names as keys and image paths as values
|
| 234 |
+
"""
|
| 235 |
+
# Extract key frames from user video
|
| 236 |
+
user_frames = extract_key_swing_frames(user_video_path, user_swing_phases)
|
| 237 |
+
|
| 238 |
+
# Get pro frames either from provided images or video
|
| 239 |
+
if use_pro_images:
|
| 240 |
+
pro_frames = load_pro_reference_images()
|
| 241 |
+
else:
|
| 242 |
+
pro_frames = extract_key_swing_frames(pro_video_path, pro_swing_phases)
|
| 243 |
+
|
| 244 |
+
# Create output directory with absolute path
|
| 245 |
+
output_dir = os.path.abspath(output_dir)
|
| 246 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 247 |
+
|
| 248 |
+
comparison_data = {}
|
| 249 |
+
phases = ['setup', 'backswing', 'impact']
|
| 250 |
+
phase_titles = ['Starting Position', 'Top of Backswing', 'Impact with Ball']
|
| 251 |
+
|
| 252 |
+
for i, phase in enumerate(phases):
|
| 253 |
+
# Get frames for this phase
|
| 254 |
+
user_frame = user_frames.get(phase, np.zeros((480, 640, 3), dtype=np.uint8))
|
| 255 |
+
pro_frame = pro_frames.get(phase, np.zeros((480, 640, 3), dtype=np.uint8))
|
| 256 |
+
|
| 257 |
+
# Resize frames to consistent size while maintaining portrait orientation
|
| 258 |
+
target_height = 400
|
| 259 |
+
user_frame = resize_frame_maintain_aspect(user_frame, target_height)
|
| 260 |
+
pro_frame = resize_frame_maintain_aspect(pro_frame, target_height)
|
| 261 |
+
|
| 262 |
+
# Create side-by-side comparison
|
| 263 |
+
comparison_image = create_side_by_side_image(user_frame, pro_frame, phase_titles[i])
|
| 264 |
+
|
| 265 |
+
# Save the comparison image with absolute path
|
| 266 |
+
video_name = os.path.splitext(os.path.basename(user_video_path))[0]
|
| 267 |
+
output_path = os.path.join(output_dir, f"{video_name}_{phase}_comparison.jpg")
|
| 268 |
+
|
| 269 |
+
# Ensure the image is saved successfully
|
| 270 |
+
success = cv2.imwrite(output_path, comparison_image)
|
| 271 |
+
if not success:
|
| 272 |
+
print(f"Warning: Failed to save image to {output_path}")
|
| 273 |
+
else:
|
| 274 |
+
print(f"Successfully saved comparison image: {output_path}")
|
| 275 |
+
|
| 276 |
+
# Get improvement comments
|
| 277 |
+
comments = generate_improvement_comments(phase)
|
| 278 |
+
|
| 279 |
+
comparison_data[phase] = {
|
| 280 |
+
'image_path': output_path,
|
| 281 |
+
'title': phase_titles[i],
|
| 282 |
+
'comments': comments
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
return comparison_data
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
def resize_frame_maintain_aspect(frame, target_height):
|
| 289 |
+
"""
|
| 290 |
+
Resize frame to target height while maintaining aspect ratio
|
| 291 |
+
|
| 292 |
+
Args:
|
| 293 |
+
frame (numpy.ndarray): Input frame
|
| 294 |
+
target_height (int): Target height
|
| 295 |
+
|
| 296 |
+
Returns:
|
| 297 |
+
numpy.ndarray: Resized frame
|
| 298 |
+
"""
|
| 299 |
+
h, w = frame.shape[:2]
|
| 300 |
+
target_width = int(w * (target_height / h))
|
| 301 |
+
return cv2.resize(frame, (target_width, target_height))
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def create_side_by_side_image(user_frame, pro_frame, title):
|
| 305 |
+
"""
|
| 306 |
+
Create a side-by-side comparison image
|
| 307 |
+
|
| 308 |
+
Args:
|
| 309 |
+
user_frame (numpy.ndarray): User's swing frame
|
| 310 |
+
pro_frame (numpy.ndarray): Pro's swing frame
|
| 311 |
+
title (str): Title for the comparison
|
| 312 |
+
|
| 313 |
+
Returns:
|
| 314 |
+
numpy.ndarray: Combined comparison image
|
| 315 |
+
"""
|
| 316 |
+
# Get dimensions
|
| 317 |
+
user_h, user_w = user_frame.shape[:2]
|
| 318 |
+
pro_h, pro_w = pro_frame.shape[:2]
|
| 319 |
+
|
| 320 |
+
# Create padding and title space
|
| 321 |
+
padding = 20
|
| 322 |
+
title_height = 60
|
| 323 |
+
max_height = max(user_h, pro_h)
|
| 324 |
+
total_width = user_w + pro_w + padding
|
| 325 |
+
total_height = max_height + title_height
|
| 326 |
+
|
| 327 |
+
# Create blank canvas
|
| 328 |
+
canvas = np.ones((total_height, total_width, 3), dtype=np.uint8) * 255
|
| 329 |
+
|
| 330 |
+
# Add title
|
| 331 |
+
font = cv2.FONT_HERSHEY_SIMPLEX
|
| 332 |
+
title_size = cv2.getTextSize(title, font, 1.2, 2)[0]
|
| 333 |
+
title_x = (total_width - title_size[0]) // 2
|
| 334 |
+
cv2.putText(canvas, title, (title_x, 40), font, 1.2, (0, 0, 0), 2)
|
| 335 |
+
|
| 336 |
+
# Add user frame
|
| 337 |
+
y_offset = title_height + (max_height - user_h) // 2
|
| 338 |
+
canvas[y_offset:y_offset + user_h, 0:user_w] = user_frame
|
| 339 |
+
|
| 340 |
+
# Add pro frame
|
| 341 |
+
y_offset = title_height + (max_height - pro_h) // 2
|
| 342 |
+
canvas[y_offset:y_offset + pro_h, user_w + padding:user_w + padding + pro_w] = pro_frame
|
| 343 |
+
|
| 344 |
+
# Draw vertical separator line
|
| 345 |
+
line_x = user_w + padding // 2
|
| 346 |
+
cv2.line(canvas, (line_x, title_height), (line_x, total_height), (200, 200, 200), 2)
|
| 347 |
+
|
| 348 |
+
return canvas
|
| 349 |
+
|
| 350 |
+
|
| 351 |
def normalize_frames(frames, target_height=480):
|
| 352 |
"""
|
| 353 |
Normalize frames to a consistent size while maintaining aspect ratio
|
app/utils/visualizer.py
CHANGED
|
@@ -94,14 +94,11 @@ def create_annotated_video(video_path,
|
|
| 94 |
elif rotation_value == 270: # 270 degrees clockwise
|
| 95 |
rotation = 90 # We'll rotate counterclockwise, so 90
|
| 96 |
except:
|
| 97 |
-
# If metadata reading fails,
|
| 98 |
rotation = 0
|
| 99 |
|
| 100 |
-
#
|
| 101 |
-
|
| 102 |
-
# Check if video is in portrait mode (height > width)
|
| 103 |
-
if height > width * 1.2: # If height is significantly greater than width
|
| 104 |
-
rotation = 90 # Rotate 90 degrees counterclockwise
|
| 105 |
|
| 106 |
# Close the video capture
|
| 107 |
cap.release()
|
|
|
|
| 94 |
elif rotation_value == 270: # 270 degrees clockwise
|
| 95 |
rotation = 90 # We'll rotate counterclockwise, so 90
|
| 96 |
except:
|
| 97 |
+
# If metadata reading fails, don't apply any rotation
|
| 98 |
rotation = 0
|
| 99 |
|
| 100 |
+
# Don't apply automatic rotation based on dimensions
|
| 101 |
+
# Keep the video in its original orientation
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
# Close the video capture
|
| 104 |
cap.release()
|