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)