foot_video_stat / modules /team_classifier.py
simonlesaumon's picture
Upload folder using huggingface_hub
497fce0 verified
Raw
History Blame Contribute Delete
5.15 kB
import cv2
import numpy as np
from sklearn.cluster import KMeans
import football_analytics.config as config
class TeamClassifier:
def __init__(self, n_clusters=2, distance_threshold=45.0):
"""
Unsupervised jersey color classifier using KMeans.
Args:
n_clusters (int): Number of teams to cluster (default 2)
distance_threshold (float): Euclidean distance threshold in RGB space (0-255)
above which a player is classified as 'Other' (referee/goalkeeper).
"""
self.n_clusters = n_clusters
self.distance_threshold = distance_threshold
self.kmeans = KMeans(n_clusters=n_clusters, n_init=10, random_state=42)
self.is_fitted = False
self.team_colors = {} # Store cluster centers for visual annotation
self.features_buffer = []
def _extract_jersey_color(self, frame, bbox):
"""
Crops the jersey area and returns the average RGB color.
"""
h_frame, w_frame, _ = frame.shape
x1, y1, x2, y2 = bbox
# Clip to frame boundaries
x1, y1 = max(0, int(x1)), max(0, int(y1))
x2, y2 = min(w_frame, int(x2)), min(h_frame, int(y2))
w = x2 - x1
h = y2 - y1
if w <= 0 or h <= 0:
return None
# Get chest region based on config ratios
ymin_j = int(y1 + h * config.JERSEY_BOX[0])
ymax_j = int(y1 + h * config.JERSEY_BOX[2])
xmin_j = int(x1 + w * config.JERSEY_BOX[1])
xmax_j = int(x1 + w * config.JERSEY_BOX[3])
crop = frame[ymin_j:ymax_j, xmin_j:xmax_j]
if crop.size == 0:
return None
# Convert BGR to RGB
crop_rgb = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB)
# Calculate mean RGB color of the chest region
mean_color = crop_rgb.mean(axis=(0, 1))
return mean_color
def collect_features(self, frame, detections):
"""
Extracts jersey color features for all player detections in a frame and adds to buffer.
"""
for bbox, class_id in zip(detections.xyxy, detections.class_id):
if class_id == 0: # Person (Player/Referee)
color = self._extract_jersey_color(frame, bbox)
if color is not None:
self.features_buffer.append(color)
def fit(self):
"""
Fits the KMeans model on the accumulated color features.
"""
if getattr(config, "USE_SUPERVISED_TEAMS", False):
print("[Team Classifier] Supervised team mode enabled. Using predefined target colors:")
self.team_colors = {}
for k, v in config.SUPERVISED_COLORS.items():
print(f" Team {k} RGB: {v}")
self.team_colors[k] = np.array(v)
self.is_fitted = True
return True
if len(self.features_buffer) < self.n_clusters * 10:
print(f"[Team Classifier] Warning: Not enough player detections to fit KMeans ({len(self.features_buffer)} found). Using fallback colors.")
# Create a mock fit or use default centers
self.team_colors = {0: np.array([255, 0, 0]), 1: np.array([0, 0, 255])}
self.is_fitted = True
return False
features = np.array(self.features_buffer)
self.kmeans.fit(features)
self.is_fitted = True
# Store team average colors
for i in range(self.n_clusters):
self.team_colors[i] = self.kmeans.cluster_centers_[i]
print(f"[Team Classifier] Successfully fitted K-Means on {len(features)} detections.")
print(f"Team 0 Center Color (RGB): {self.team_colors[0].astype(int)}")
print(f"Team 1 Center Color (RGB): {self.team_colors[1].astype(int)}")
return True
def predict_team(self, frame, bbox):
"""
Predicts the team of a player based on jersey color.
Returns:
int: 0 for Team A, 1 for Team B, 2 for Referee/Other
"""
if not self.is_fitted:
return 0 # Default to Team 0 if not fitted
color = self._extract_jersey_color(frame, bbox)
if color is None:
return 0
if getattr(config, "USE_SUPERVISED_TEAMS", False):
# Supervised classification: find closest of the pre-defined target colors
dists = {
k: np.linalg.norm(color - np.array(v))
for k, v in config.SUPERVISED_COLORS.items()
}
return min(dists, key=dists.get)
# Calculate distances to all K-Means cluster centers
distances = [np.linalg.norm(color - center) for center in self.kmeans.cluster_centers_]
min_dist = min(distances)
predicted_cluster = np.argmin(distances)
# If distance to nearest team is too large, classify as referee/other (class ID 2)
if min_dist > self.distance_threshold:
return 2
return int(predicted_cluster)