Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • lokeshsat01/capstone-project
  • ashutoshpocham/capstone-project
  • ggali14/capstone-project
3 results
Show changes
Showing
with 20096 additions and 0 deletions
{% extends "base.html" %}
{% block content %}
<div class="auth-container">
<h2>Login</h2>
<form action="/login" method="POST">
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
<button type="submit">Login</button>
</form>
<p>Don't have an account? <a href="/register">Register here</a>.</p>
</div>
{% endblock %}
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Match Playback and Player Stats</title>
</head>
<style>
pre {
background-color: #f8f9fa;
padding: 10px;
border: 1px solid #dee2e6;
border-radius: 5px;
overflow: auto;
font-size: 14px;
color: #343a40;
}
</style>
<body>
{% extends "base.html" %}
{% block content %}
<h1>Match Playback and Player Stats</h1>
<!-- Video Playback Section -->
<div class="video-container">
<h2>Video Playback</h2>
<p>Player 1: {{ player1 }}</p>
<p>Player 2: {{ player2 }}</p>
<video controls style="width: 100%; max-width: 600px; height: auto;">
<source src="{{ video_url }}" type="video/mp4">
Your browser does not support the video tag.
</video>
<video controls style="width: 100%; max-width: 600px; height: auto;">
<source src="{{ output_video_url | safe }}" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
<!-- Player Statistics Section -->
<div class="stats-container">
<h2>Player Comparison</h2>
<table>
<thead>
<tr>
<th>Stat</th>
<th>{{ player1 }}</th>
<th>{{ player2 }}</th>
</tr>
</thead>
<tbody>
{% for stat, values in stats.items() %}
<tr>
<td>{{ stat }}</td>
<td>{{ values[0] }}</td>
<td>{{ values[1] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Player Overview Section -->
<div class="stats-container">
<h2>Player Overview</h2>
<table>
<thead>
<tr>
<th>Stat</th>
<th>{{ player1 }}</th>
<th>{{ player2 }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>Full Name</td>
<td>{{ player1_data.playerOverview.fullName }}</td>
<td>{{ player2_data.playerOverview.fullName }}</td>
</tr>
<tr>
<td>Nationality</td>
<td>{{ player1_data.playerOverview.nationality }}</td>
<td>{{ player2_data.playerOverview.nationality }}</td>
</tr>
<tr>
<td>Date of Birth</td>
<td>{{ player1_data.playerOverview.dateOfBirth }}</td>
<td>{{ player2_data.playerOverview.dateOfBirth }}</td>
</tr>
<tr>
<td>Height</td>
<td>{{ player1_data.playerOverview.height }}</td>
<td>{{ player2_data.playerOverview.height }}</td>
</tr>
<tr>
<td>Weight</td>
<td>{{ player1_data.playerOverview.weight }}</td>
<td>{{ player2_data.playerOverview.weight }}</td>
</tr>
<tr>
<td>Playing Hand</td>
<td>{{ player1_data.playerOverview.playingHand }}</td>
<td>{{ player2_data.playerOverview.playingHand }}</td>
</tr>
<tr>
<td>Playing Style</td>
<td>{{ player1_data.playerOverview.playingStyle }}</td>
<td>{{ player2_data.playerOverview.playingStyle }}</td>
</tr>
<tr>
<td>Turned Pro Year</td>
<td>{{ player1_data.playerOverview.turnedProYear }}</td>
<td>{{ player2_data.playerOverview.turnedProYear }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Career Highlights Section -->
<div class="stats-container">
<h2>Career Highlights</h2>
<table>
<thead>
<tr>
<th>Stat</th>
<th>{{ player1 }}</th>
<th>{{ player2 }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>Total Career Titles</td>
<td>{{ player1_data.careerHighlights.totalCareerTitles }}</td>
<td>{{ player2_data.careerHighlights.totalCareerTitles }}</td>
</tr>
<tr>
<td>Grand Slam Wins</td>
<td>{{ player1_data.careerHighlights.grandSlamWins }}</td>
<td>{{ player2_data.careerHighlights.grandSlamWins }}</td>
</tr>
<tr>
<td>Olympic Medals</td>
<td>{{ player1_data.careerHighlights.olympicMedals.details }}</td>
<td>{{ player2_data.careerHighlights.olympicMedals.details or 'N/A' }}</td>
</tr>
<tr>
<td>Career Prize Money</td>
<td>${{ player1_data.careerHighlights.careerPrizeMoney }}</td>
<td>${{ player2_data.careerHighlights.careerPrizeMoney }}</td>
</tr>
<tr>
<td>Career-High Ranking</td>
<td>{{ player1_data.careerHighlights.careerHighRanking }}</td>
<td>{{ player2_data.careerHighlights.careerHighRanking }}</td>
</tr>
<tr>
<td>Current Ranking</td>
<td>{{ player1_data.careerHighlights.currentRanking }}</td>
<td>{{ player2_data.careerHighlights.currentRanking }}</td>
</tr>
<tr>
<td>Year-End No. 1 Rankings</td>
<td>{{ player1_data.careerHighlights.yearEndNo1Rankings | join(', ') }}</td>
<td>{{ player2_data.careerHighlights.yearEndNo1Rankings | join(', ') }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Head-to-Head Records Section -->
<div class="stats-container">
<h2>Head-to-Head Records</h2>
<table>
<thead>
<tr>
<th>Stat</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for key, value in player1_data.headToHeadRecords.items() %}
<tr>
<td>{{ key | replace('_', ' ') | capitalize }}</td>
<td>
{% if value is mapping %}
<pre>{{ value | tojson(indent=2) }}</pre>
{% else %}
{{ value }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Performance Statistics Section -->
<div class="stats-container">
<h2>Performance Statistics</h2>
<table>
<thead>
<tr>
<th>Stat</th>
<th>{{ player1 }}</th>
<th>{{ player2 }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>Overall Win Percentage</td>
<td>{{ player1_data.performanceStatistics.overallWinPercentage }}%</td>
<td>{{ player2_data.performanceStatistics.overallWinPercentage }}%</td>
</tr>
<tr>
<td>Aces</td>
<td>{{ player1_data.performanceStatistics.serviceStats.aces }}</td>
<td>{{ player2_data.performanceStatistics.serviceStats.aces }}</td>
</tr>
<tr>
<td>Double Faults</td>
<td>{{ player1_data.performanceStatistics.serviceStats.doubleFaults }}</td>
<td>{{ player2_data.performanceStatistics.serviceStats.doubleFaults }}</td>
</tr>
<tr>
<td>First Serve Percentage</td>
<td>{{ player1_data.performanceStatistics.serviceStats.firstServePercentage }}%</td>
<td>{{ player2_data.performanceStatistics.serviceStats.firstServePercentage }}%</td>
</tr>
<tr>
<td>First Serve Points Won</td>
<td>{{ player1_data.performanceStatistics.serviceStats.firstServePointsWon }}%</td>
<td>{{ player2_data.performanceStatistics.serviceStats.firstServePointsWon }}%</td>
</tr>
<tr>
<td>Second Serve Points Won</td>
<td>{{ player1_data.performanceStatistics.serviceStats.secondServePointsWon }}%</td>
<td>{{ player2_data.performanceStatistics.serviceStats.secondServePointsWon }}%</td>
</tr>
<tr>
<td>Break Points Converted</td>
<td>{{ player1_data.performanceStatistics.returnStats.firstServeReturnPointsWon }}%</td>
<td>{{ player2_data.performanceStatistics.returnStats.firstServeReturnPointsWon }}%</td>
</tr>
<tr>
<td>Second Break Points Converted</td>
<td>{{ player1_data.performanceStatistics.returnStats.secondServeReturnPointsWon }}%</td>
<td>{{ player2_data.performanceStatistics.returnStats.secondServeReturnPointsWon }}%</td>
</tr>
<tr>
<td>Tiebreak Record</td>
<td>{{ player1_data.performanceStatistics.tiebreakRecord }}</td>
<td>{{ player2_data.performanceStatistics.tiebreakRecord }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Tournament History Section -->
<div class="stats-container">
<h2>Tournament History</h2>
<table>
<thead>
<tr>
<th>Tournament</th>
<th>{{ player1 }}</th>
<th>{{ player2 }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>Australian Open</td>
<td>{{ player1_data.tournamentHistory.grandSlamPerformances.australianOpen }}</td>
<td>{{ player2_data.tournamentHistory.grandSlamPerformances.australianOpen }}</td>
</tr>
<tr>
<td>French Open</td>
<td>{{ player1_data.tournamentHistory.grandSlamPerformances.frenchOpen }}</td>
<td>{{ player2_data.tournamentHistory.grandSlamPerformances.frenchOpen }}</td>
</tr>
<tr>
<td>Wimbledon</td>
<td>{{ player1_data.tournamentHistory.grandSlamPerformances.wimbledon }}</td>
<td>{{ player2_data.tournamentHistory.grandSlamPerformances.wimbledon }}</td>
</tr>
<tr>
<td>US Open</td>
<td>{{ player1_data.tournamentHistory.grandSlamPerformances.usOpen }}</td>
<td>{{ player2_data.tournamentHistory.grandSlamPerformances.usOpen }}</td>
</tr>
<tr>
<td>ATP Finals Titles</td>
<td>{{ player1_data.tournamentHistory.atpFinalsTitles }}</td>
<td>{{ player2_data.tournamentHistory.atpFinalsTitles }}</td>
</tr>
<tr>
<td>Masters Titles</td>
<td>{{ player1_data.tournamentHistory.mastersTitles }}</td>
<td>{{ player2_data.tournamentHistory.mastersTitles }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Historical Trends Section -->
<div class="stats-container">
<h2>Historical Trends</h2>
<ul>
<li>{{ player1 }} Match-Win Streaks: {{ player1_data.historicalTrends.matchWinStreaks }}</li>
<li>{{ player2 }} Match-Win Streaks: {{ player2_data.historicalTrends.matchWinStreaks }}</li>
<li>{{ player1 }} Performance Against Top-10 Players: {{ player1_data.historicalTrends.performanceAgainstTop10Players }}</li>
<li>{{ player2 }} Performance Against Top-10 Players: {{ player2_data.historicalTrends.performanceAgainstTop10Players }}</li>
<li>{{ player1 }} Notable Upsets: {{ player1_data.historicalTrends.notableUpsets }}</li>
<li>{{ player2 }} Notable Upsets: {{ player2_data.historicalTrends.notableUpsets }}</li>
</ul>
</div>
<!-- Graph Section -->
<div class="graphs-container">
<h2>Comparison Visualizations</h2>
<div class="graphs">
{% for graph_name, graph_path in graph_paths.items() %}
<div class="graph">
<h3>{{ graph_name.replace('_', ' ').title() }}</h3>
<img src="{{ graph_path }}" alt="{{ graph_name }}">
</div>
{% endfor %}
</div>
</div>
{% endblock %}
</body>
</html>
{% extends "base.html" %}
{% block content %}
<h1>{{ player.get('name', 'Player Name Not Available') }}</h1>
<h2>Profile</h2>
<p><strong>Ranking:</strong> {{ player.get('ranking', 'N/A') }}</p>
<p><strong>Country:</strong> {{ player.get('country', {}).get('name', 'N/A') }}</p>
<p><strong>Gender:</strong> {{ player.get('gender', 'N/A') }}</p>
<p><strong>Short Name:</strong> {{ player.get('shortName', 'N/A') }}</p>
<p><strong>Slug:</strong> {{ player.get('slug', 'N/A') }}</p>
<p><strong>User Count:</strong> {{ player.get('userCount', 'N/A') }}</p>
<p><strong>Primary Color:</strong> {{ player.get('teamColors', {}).get('primary', 'N/A') }}</p>
<p><strong>Secondary Color:</strong> {{ player.get('teamColors', {}).get('secondary', 'N/A') }}</p>
{% endblock %}
{% extends "base.html" %}
{% block content %}
<h2>ATP Player Rankings</h2>
<table>
<thead>
<tr>
<th>Rank</th>
<th>Player Name</th>
<th>Points</th>
<th>Country</th>
</tr>
</thead>
<tbody>
{% for player in rankings %}
<tr>
<td>{{ player.ranking }}</td>
<!-- Format player name for search (e.g., 'Sinner J.') -->
<td>
<a href="/player_search?query={{ player.team.name.split(' ')[0] }}">
{{ player.team.name }}
</a>
</td>
<td>{{ player.points }}</td>
<td>{{ player.team.country.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
{% extends "base.html" %}
{% block content %}
<div class="auth-container">
<h2>Register</h2>
<form action="/register" method="POST">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
<button type="submit">Register</button>
</form>
<p>Already have an account? <a href="/login">Login here</a>.</p>
</div>
{% endblock %}
{% extends "base.html" %}
{% block content %}
<h1>Tennis Schedule</h1>
<table>
<thead>
<tr>
<th>Home Player</th>
<th>Away Player</th>
<th>Home Player Score</th>
<th>Away Player Score</th>
<th>Ground Type</th>
</tr>
</thead>
<tbody>
{% for event in events %}
<tr>
<td>{{ event.homeTeam.name }}</td>
<td>{{ event.awayTeam.name }}</td>
<td>{{ event.homeScore.current }}</td>
<td>{{ event.awayScore.current }}</td>
<td>{{ event.groundType }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
File added
File added
from .player_tracker import PlayerTracker
from .ball_tracker import BallTracker
from .pose_detector import PoseDetector
\ No newline at end of file
from ultralytics import YOLO
import cv2
import pickle
import pandas as pd
class BallTracker:
def __init__(self, model_path):
self.model = YOLO(model_path)
def interpolate_ball_positions(self, ball_positions):
ball_positions = [x.get(1, []) for x in ball_positions]
# Convert the list into a DataFrame
df_ball_positions = pd.DataFrame(ball_positions, columns=['x1', 'y1', 'x2', 'y2'])
# Interpolate missing values
df_ball_positions = df_ball_positions.interpolate().bfill()
# Convert back to list of dictionaries
ball_positions = [{1: x} for x in df_ball_positions.to_numpy().tolist()]
return ball_positions
def get_ball_shot_frames(self, ball_positions):
ball_positions = [x.get(1, []) for x in ball_positions]
df_ball_positions = pd.DataFrame(ball_positions, columns=['x1', 'y1', 'x2', 'y2'])
# Initialize ball hit column and compute mid_y and delta_y
df_ball_positions['ball_hit'] = 0
df_ball_positions['mid_y'] = (df_ball_positions['y1'] + df_ball_positions['y2']) / 2
df_ball_positions['mid_y_rolling_mean'] = df_ball_positions['mid_y'].rolling(window=5, min_periods=1).mean()
df_ball_positions['delta_y'] = df_ball_positions['mid_y_rolling_mean'].diff()
minimum_change_frames_for_hit = 25
# Detect ball hits based on delta_y changes
for i in range(1, len(df_ball_positions) - int(minimum_change_frames_for_hit * 1.2)):
negative_change = df_ball_positions['delta_y'].iloc[i] > 0 and df_ball_positions['delta_y'].iloc[i + 1] < 0
positive_change = df_ball_positions['delta_y'].iloc[i] < 0 and df_ball_positions['delta_y'].iloc[i + 1] > 0
if negative_change or positive_change:
change_count = 0
for change_frame in range(i + 1, i + int(minimum_change_frames_for_hit * 1.2) + 1):
negative_following = df_ball_positions['delta_y'].iloc[i] > 0 and df_ball_positions['delta_y'].iloc[change_frame] < 0
positive_following = df_ball_positions['delta_y'].iloc[i] < 0 and df_ball_positions['delta_y'].iloc[change_frame] > 0
if negative_change and negative_following:
change_count += 1
elif positive_change and positive_following:
change_count += 1
# Use .loc to avoid chained assignment warning
if change_count > minimum_change_frames_for_hit - 1:
df_ball_positions.loc[i, 'ball_hit'] = 1
# Return frame numbers with ball hits
return df_ball_positions[df_ball_positions['ball_hit'] == 1].index.tolist()
def detect_frames(self, frames, read_from_stub=False, stub_path=None):
ball_detections = []
if read_from_stub and stub_path:
with open(stub_path, 'rb') as f:
ball_detections = pickle.load(f)
return ball_detections
for frame in frames:
detection = self.detect_frame(frame)
ball_detections.append(detection)
if stub_path:
with open(stub_path, 'wb') as f:
pickle.dump(ball_detections, f)
return ball_detections
def detect_frame(self, frame):
results = self.model.predict(frame, conf=0.15)[0]
ball_dict = {}
for box in results.boxes:
result = box.xyxy.tolist()[0]
ball_dict[1] = result
return ball_dict
def draw_bboxes(self, video_frames, player_detections):
output_video_frames = []
for frame, ball_dict in zip(video_frames, player_detections):
for track_id, bbox in ball_dict.items():
x1, y1, x2, y2 = bbox
cv2.putText(frame, f"Ball ID: {track_id}", (int(x1), int(y1 - 10)),
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 255), 2)
cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 255), 2)
output_video_frames.append(frame)
return output_video_frames
from ultralytics import YOLO
import cv2
import pickle
import sys
sys.path.append('../')
from utils import measure_distance, get_center_of_bbox
class PlayerTracker:
def __init__(self,model_path):
self.model = YOLO(model_path)
def choose_and_filter_players(self, court_keypoints, player_detections):
player_detections_first_frame = player_detections[0]
chosen_player = self.choose_players(court_keypoints, player_detections_first_frame)
filtered_player_detections = []
for player_dict in player_detections:
filtered_player_dict = {track_id: bbox for track_id, bbox in player_dict.items() if track_id in chosen_player}
filtered_player_detections.append(filtered_player_dict)
return filtered_player_detections
def choose_players(self, court_keypoints, player_dict):
distances = []
for track_id, bbox in player_dict.items():
player_center = get_center_of_bbox(bbox)
min_distance = float('inf')
for i in range(0,len(court_keypoints),2):
court_keypoint = (court_keypoints[i], court_keypoints[i+1])
distance = measure_distance(player_center, court_keypoint)
if distance < min_distance:
min_distance = distance
distances.append((track_id, min_distance))
# sorrt the distances in ascending order
distances.sort(key = lambda x: x[1])
# Choose the first 2 tracks
chosen_players = [distances[0][0], distances[1][0]]
return chosen_players
def detect_frames(self,frames, read_from_stub=False, stub_path=None):
player_detections = []
if read_from_stub and stub_path is not None:
with open(stub_path, 'rb') as f:
player_detections = pickle.load(f)
return player_detections
for frame in frames:
player_dict = self.detect_frame(frame)
player_detections.append(player_dict)
if stub_path is not None:
with open(stub_path, 'wb') as f:
pickle.dump(player_detections, f)
return player_detections
def detect_frame(self, frame):
"""Detect players and their keypoints in a single frame."""
results = self.model.track(frame, persist=True)[0]
id_name_dict = results.names
player_dict = {}
keypoints_dict = {}
for box in results.boxes:
track_id = int(box.id.tolist()[0])
bbox = box.xyxy.tolist()[0]
object_cls_id = box.cls.tolist()[0]
object_cls_name = id_name_dict[object_cls_id]
if object_cls_name == "person":
player_dict[track_id] = bbox
# Extract keypoints if available
if hasattr(box, 'keypoints'):
keypoints_dict[track_id] = box.keypoints.cpu().numpy()
return player_dict, keypoints_dict
def draw_bboxes(self,video_frames, player_detections):
output_video_frames = []
for frame, (player_dict, keypoints_dict) in zip(video_frames, player_detections):
for track_id, bbox in player_dict.items():
x1, y1, x2, y2 = bbox
# Draw the bounding box
cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2)
cv2.putText(frame, f"Player ID: {track_id}",
(int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 0, 255), 2)
# Draw skeleton if keypoints are available
if track_id in keypoints_dict:
keypoints = keypoints_dict[track_id]
self.draw_skeleton(frame, keypoints)
output_video_frames.append(frame)
return output_video_frames
def draw_skeleton(self, frame, keypoints):
"""
Draws the skeleton on the provided frame using keypoints.
Args:
frame (np.ndarray): The video frame to draw on.
keypoints (np.ndarray): Array of shape (N, 3), where N is the number of keypoints.
Each keypoint is represented by (x, y, confidence).
"""
# Define the pairs of keypoints that form the skeleton
skeleton_pairs = [
(0, 1), (1, 2), (2, 3), (3, 4), # Head to shoulders
(5, 6), (5, 7), (7, 9), # Left arm
(6, 8), (8, 10), # Right arm
(11, 12), (11, 13), (13, 15), # Left leg
(12, 14), (14, 16) # Right leg
]
# Iterate over the skeleton pairs and draw lines between connected keypoints
for start_idx, end_idx in skeleton_pairs:
x1, y1, c1 = keypoints[start_idx]
x2, y2, c2 = keypoints[end_idx]
# Only draw if both keypoints are visible with confidence > 0.5
if c1 > 0.5 and c2 > 0.5:
# Draw the skeleton line between the two keypoints
cv2.line(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2)
# Draw circles on each keypoint
for x, y, c in keypoints:
if c > 0.5: # Only draw if confidence > 0.5
cv2.circle(frame, (int(x), int(y)), 5, (255, 0, 0), -1) # Blue dot for keypoints
\ No newline at end of file
import torch
import cv2
import numpy as np
from torchvision import transforms, models
from PIL import Image
class PoseDetector:
def __init__(self, model_path):
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
self.model = self._load_model(model_path)
self.model.eval()
self.pose_transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
def _load_model(self, model_path):
model = PoseEstimationModel(num_keypoints=18, num_classes=4)
model.load_state_dict(torch.load(model_path, map_location=self.device))
model.to(self.device)
return model
def crop_and_predict_pose(self, frame, bbox):
"""Crop the player from the frame and predict keypoints and pose class."""
x1, y1, x2, y2 = map(int, bbox)
cropped_frame = frame[y1:y2, x1:x2]
# Convert to RGB and apply transformations
image = cv2.cvtColor(cropped_frame, cv2.COLOR_BGR2RGB)
image = self.pose_transform(Image.fromarray(image)).unsqueeze(0).to(self.device)
# Perform inference
with torch.no_grad():
keypoints, class_logits = self.model(image)
_, predicted_class = torch.max(class_logits, 1)
# Convert keypoints to numpy and adjust scaling
keypoints = keypoints.squeeze(0).cpu().numpy()
# Scale keypoints back to original bbox size
keypoints[:, 0] = keypoints[:, 0] * (x2 - x1) / 224 + x1 # Scale X
keypoints[:, 1] = keypoints[:, 1] * (y2 - y1) / 224 + y1 # Scale Y
predicted_class = predicted_class.item()
return keypoints, predicted_class
def draw_poses(self, video_frames, player_detections, pose_predictions):
output_frames = []
for frame, player_dict in zip(video_frames, player_detections):
for player_id, bbox in player_dict.items():
if player_id in pose_predictions:
keypoints, pose_class = pose_predictions[player_id]
# Ensure keypoints are valid and correctly formatted
if isinstance(keypoints, np.ndarray) and keypoints.shape == (18, 2):
for x, y in keypoints:
if 0 <= x < frame.shape[1] and 0 <= y < frame.shape[0]: # Check bounds
cv2.circle(frame, (int(x), int(y)), 5, (0, 255, 0), -1)
x1, y1, _, _ = map(int, bbox)
pose_label = ["Backhand", "Forehand", "Ready", "Serve"][pose_class]
cv2.putText(frame, f"{pose_label}", (x1, y1 - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
else:
print(f"Invalid keypoints for player {player_id}: {keypoints}")
output_frames.append(frame)
return output_frames
def draw_pose(self, frame, bbox, keypoints, pose_class):
"""Draw keypoints and pose label on the player in the frame."""
# Ensure bbox values are integers
x1, y1, x2, y2 = map(int, bbox)
# Draw keypoints on the player
for (x, y) in keypoints:
if 0 <= int(x) < frame.shape[1] and 0 <= int(y) < frame.shape[0]: # Check if keypoints are within bounds
cv2.circle(frame, (int(x), int(y)), 5, (0, 255, 0), -1) # Draw green circle for keypoint
# Draw the pose label above the bounding box
pose_label = ["Backhand", "Forehand", "Ready", "Serve"][pose_class]
cv2.putText(frame, f"{pose_label}", (x1, y1 - 10), # Use x1, y1 as origin
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
return frame # Ensure the modified frame is returned
class PoseEstimationModel(torch.nn.Module):
def __init__(self, num_keypoints=18, num_classes=4):
super(PoseEstimationModel, self).__init__()
# Use ResNet-18 as backbone
self.backbone = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)
num_features = self.backbone.fc.in_features
# Replace the original fully connected layer with identity
self.backbone.fc = torch.nn.Identity()
# Define the keypoint and classification heads
self.keypoint_head = torch.nn.Linear(num_features, num_keypoints * 2)
self.classification_head = torch.nn.Linear(num_features, num_classes)
def forward(self, x):
features = self.backbone(x) # Extract features
keypoints = self.keypoint_head(features).view(-1, 18, 2) # (batch, 18, 2)
class_logits = self.classification_head(features) # (batch, num_classes)
return keypoints, class_logits
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
from .video_utils import read_video, save_video
from .bbox_utils import get_center_of_bbox, measure_distance, get_foot_position,get_closest_keypoint_index,get_height_of_bbox,measure_xy_distance,get_center_of_bbox
from .conversions import convert_pixel_distance_to_meters, convert_meters_to_pixel_distance
from .player_stats_drawer_utils import draw_player_stats
\ No newline at end of file
def get_center_of_bbox(bbox):
x1, y1, x2, y2 = bbox
center_x = int((x1 + x2) / 2)
center_y = int((y1 + y2) / 2)
return (center_x, center_y)
def measure_distance(p1,p2):
return ((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2)**0.5
def get_foot_position(bbox):
x1, y1, x2, y2 = bbox
return (int((x1 + x2) / 2), y2)
def get_closest_keypoint_index(point, keypoints, keypoint_indices):
closest_distance = float('inf')
key_point_ind = keypoint_indices[0]
for keypoint_indix in keypoint_indices:
keypoint = keypoints[keypoint_indix*2], keypoints[keypoint_indix*2+1]
distance = abs(point[1]-keypoint[1])
if distance<closest_distance:
closest_distance = distance
key_point_ind = keypoint_indix
return key_point_ind
def get_height_of_bbox(bbox):
return bbox[3]-bbox[1]
def measure_xy_distance(p1,p2):
return abs(p1[0]-p2[0]), abs(p1[1]-p2[1])
def get_center_of_bbox(bbox):
return (int((bbox[0]+bbox[2])/2),int((bbox[1]+bbox[3])/2))
\ No newline at end of file
def convert_pixel_distance_to_meters(pixel_distance, refrence_height_in_meters, refrence_height_in_pixels):
return (pixel_distance * refrence_height_in_meters) / refrence_height_in_pixels
def convert_meters_to_pixel_distance(meters, refrence_height_in_meters, refrence_height_in_pixels):
return (meters * refrence_height_in_pixels) / refrence_height_in_meters
\ No newline at end of file
import numpy as np
import cv2
def draw_player_stats(output_video_frames, player_stats):
"""Draw player stats, including pose stats, on the video frames."""
# Ensure that we don't exceed the available frames
max_index = len(output_video_frames) - 1
player_stats = player_stats.reset_index(drop=True) # Reset index to align with frame numbers
# Iterate through stats and apply them to frames (only if in range)
for index, row in player_stats.iterrows():
if index > max_index:
print(f"Skipping frame {index} (out of range)")
continue # Skip stats if index exceeds frame range
# Extract the relevant frame
frame = output_video_frames[index]
shapes = np.zeros_like(frame, np.uint8)
# Define overlay area dimensions
width, height = 350, 300 # Adjusted to fit additional stats
start_x = frame.shape[1] - 400
start_y = frame.shape[0] - 500
end_x = start_x + width
end_y = start_y + height
# Create an overlay for stats display
overlay = frame.copy()
cv2.rectangle(overlay, (start_x, start_y), (end_x, end_y), (0, 0, 0), -1)
alpha = 0.5
cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0, frame)
# Display shot and speed stats
text = " Player 1 Player 2"
cv2.putText(frame, text, (start_x + 80, start_y + 30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2)
cv2.putText(frame, "Shot Speed", (start_x + 10, start_y + 80), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1)
cv2.putText(frame, f"{row['player_1_last_shot_speed']:.1f} km/h {row['player_2_last_shot_speed']:.1f} km/h",
(start_x + 130, start_y + 80), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
cv2.putText(frame, "Player Speed", (start_x + 10, start_y + 120), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1)
cv2.putText(frame, f"{row['player_1_last_player_speed']:.1f} km/h {row['player_2_last_player_speed']:.1f} km/h",
(start_x + 130, start_y + 120), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
cv2.putText(frame, "avg. S. Speed", (start_x + 10, start_y + 160), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1)
cv2.putText(frame, f"{row['player_1_average_shot_speed']:.1f} km/h {row['player_2_average_shot_speed']:.1f} km/h",
(start_x + 130, start_y + 160), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
cv2.putText(frame, "avg. P. Speed", (start_x + 10, start_y + 200), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1)
cv2.putText(frame, f"{row['player_1_average_player_speed']:.1f} km/h {row['player_2_average_player_speed']:.1f} km/h",
(start_x + 130, start_y + 200), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
# Display pose stats for each player
y_offset = 240
player_1_pose_counts = {
"Backhand": row['player_1_pose_backhand'],
"Forehand": row['player_1_pose_forehand'],
"Ready": row['player_1_pose_ready'],
"Serve": row['player_1_pose_serve']
}
player_2_pose_counts = {
"Backhand": row['player_2_pose_backhand'],
"Forehand": row['player_2_pose_forehand'],
"Ready": row['player_2_pose_ready'],
"Serve": row['player_2_pose_serve']
}
for pose, count in player_1_pose_counts.items():
cv2.putText(frame, f"{pose}: {count}", (start_x + 10, start_y + y_offset),
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1)
y_offset += 20
y_offset = 240
for pose, count in player_2_pose_counts.items():
cv2.putText(frame, f"{pose}: {count}", (start_x + 200, start_y + y_offset),
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1)
y_offset += 20
return output_video_frames
import cv2
import os
def read_video(video_path):
"""Reads a video and returns a list of frames."""
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise ValueError(f"Unable to open video file: {video_path}")
frames = []
while True:
ret, frame = cap.read()
if not ret:
break
frames.append(frame)
cap.release()
if not frames:
raise ValueError("No frames found in the video.")
return frames
def save_video(output_video_frames, output_video_path, fps=24):
"""Saves a list of frames as a video."""
# Ensure the output directory exists
os.makedirs(os.path.dirname(output_video_path), exist_ok=True)
# Get the dimensions from the first frame
height, width, _ = output_video_frames[0].shape
# Use a reliable codec for compatibility
fourcc = cv2.VideoWriter_fourcc(*'MJPG')
# Initialize VideoWriter
out = cv2.VideoWriter(output_video_path, fourcc, fps, (width, height))
# Write each frame to the output video
for frame in output_video_frames:
out.write(frame)
out.release()
if not os.path.isfile(output_video_path):
raise ValueError(f"Failed to save video at: {output_video_path}")