AI analysis of sports video recordings
Professional teams spend dozens of hours a week manually analyzing match video. AI analysis automates the detection of game situations, player and ball tracking, and breakdown into episodes. Instead of watching 90 minutes of video, the analyst works with automatically extracted key moments and metrics.
Detection and tracking of players on the field
import cv2
import numpy as np
from ultralytics import YOLO
from collections import defaultdict
class SportsVideoAnalyzer:
def __init__(self, sport: str, model_path: str):
# YOLOv8l дообученный на спортивных сценах
# Классы: player_team_a, player_team_b, referee, ball, goalkeeper
self.detector = YOLO(model_path)
self.sport = sport
# Гомография: пиксели → координаты поля в метрах
self.homography = None
self.field_width = 105.0 # футбол
self.field_height = 68.0
# Трекинг
self.player_tracks = {} # track_id → list of positions
self.ball_tracks = []
def set_field_homography(self, frame: np.ndarray):
"""
Калибровка: находим разметку поля (линии, точки) и вычисляем
матрицу гомографии для перевода координат.
Используем детектор разметки или ручную разметку для первого кадра.
"""
# Реальные координаты угловых точек поля
field_pts = np.float32([
[0, 0], [self.field_width, 0],
[self.field_width, self.field_height],
[0, self.field_height]
])
# Пиксельные координаты (детектируются или задаются вручную)
frame_pts = self._detect_field_corners(frame)
if frame_pts is not None:
self.homography, _ = cv2.findHomography(
np.float32(frame_pts), field_pts
)
def track_frame(self, frame: np.ndarray) -> dict:
results = self.detector.track(frame, persist=True, conf=0.45)
frame_data = {
'players': [],
'ball': None,
'referees': []
}
for box in results[0].boxes:
cls = self.detector.model.names[int(box.cls)]
bbox = list(map(int, box.xyxy[0]))
track_id = int(box.id) if box.id is not None else -1
cx = (bbox[0] + bbox[2]) / 2
cy = (bbox[1] + bbox[3]) / 2
# Пересчёт в координаты поля
field_pos = self._to_field_coords(cx, cy)
if 'player' in cls:
player_info = {
'track_id': track_id,
'team': 'A' if 'team_a' in cls else 'B',
'bbox': bbox,
'field_pos': field_pos
}
frame_data['players'].append(player_info)
if track_id not in self.player_tracks:
self.player_tracks[track_id] = []
self.player_tracks[track_id].append(field_pos)
elif 'ball' in cls:
frame_data['ball'] = {'bbox': bbox, 'field_pos': field_pos}
self.ball_tracks.append(field_pos)
return frame_data
def _to_field_coords(self, px: float, py: float) -> tuple:
if self.homography is None:
return (px, py)
pt = np.float32([[[px, py]]])
result = cv2.perspectiveTransform(pt, self.homography)
return tuple(result[0][0].tolist())
Automatic detection of key points
class KeyEventDetector:
def __init__(self):
self.ball_speed_history = []
self.formation_history = []
def detect_shot_on_goal(self, ball_tracks: list,
goal_zone: dict) -> list[dict]:
"""
Удар по воротам: мяч движется с высокой скоростью в направлении ворот.
"""
events = []
for i in range(1, len(ball_tracks)):
if ball_tracks[i] is None or ball_tracks[i-1] is None:
continue
dx = ball_tracks[i][0] - ball_tracks[i-1][0]
dy = ball_tracks[i][1] - ball_tracks[i-1][1]
speed = np.sqrt(dx**2 + dy**2) # м/кадр
if speed > 3.0: # быстрое движение мяча
target_x = ball_tracks[i][0] + dx * 10 # экстраполяция
target_y = ball_tracks[i][1] + dy * 10
# Попадает ли траектория в зону ворот?
if (goal_zone['x1'] <= target_x <= goal_zone['x2'] and
goal_zone['y1'] <= target_y <= goal_zone['y2']):
events.append({
'type': 'shot_on_goal',
'frame': i,
'ball_speed': speed,
'ball_pos': ball_tracks[i]
})
return events
def detect_pressing(self, team_positions: list,
opponent_with_ball: dict) -> float:
"""Индекс прессинга: сколько игроков в радиусе 5м от владельца мяча"""
if not opponent_with_ball or not team_positions:
return 0.0
ball_x, ball_y = opponent_with_ball['field_pos']
pressing_players = sum(
1 for p in team_positions
if np.sqrt((p[0]-ball_x)**2 + (p[1]-ball_y)**2) < 5.0
)
return pressing_players / max(len(team_positions), 1)
Player activity heat map
def generate_heatmap(player_track: list,
field_w: float = 105, field_h: float = 68,
resolution: int = 100) -> np.ndarray:
heatmap = np.zeros((resolution, int(resolution * field_w / field_h)))
for pos in player_track:
if pos is None:
continue
px = int(pos[0] / field_w * heatmap.shape[1])
py = int(pos[1] / field_h * heatmap.shape[0])
px = np.clip(px, 0, heatmap.shape[1]-1)
py = np.clip(py, 0, heatmap.shape[0]-1)
heatmap[py, px] += 1
heatmap = cv2.GaussianBlur(heatmap.astype(np.float32), (15, 15), 5)
heatmap /= max(heatmap.max(), 1)
return heatmap
Case: Professional Football Club
A First League club. Task: automated analysis of 10-15 matches per week (home and opponents). Previously: 1 video analyst, 3-4 hours per match.
After implementation:
- Automatic cutting: shots on goal, set pieces, changes of possession - in 8 minutes per match
- Heat maps of all players, running statistics (km/match, sprints)
- The analyst spends 30-45 minutes on analysis instead of 3-4 hours
| Metrics | Accuracy |
|---|---|
| Player detection | 94–97% |
| Ball tracking (visible) | 88–93% |
| Identifying a team by color | 91–96% |
| Shot detection | 86–92% |
| Project type | Term |
|---|---|
| Basic player tracking + heatmaps | 5–8 weeks |
| Complete analytical system | 10–16 weeks |







