Player tracking technology has revolutionized football analytics by providing spatial and temporal data at unprecedented granularity. This chapter explores how computer vision and tracking systems capture player movements, and how to analyze this...
In This Chapter
Chapter 24: Computer Vision and Tracking Data
Introduction
Player tracking technology has revolutionized football analytics by providing spatial and temporal data at unprecedented granularity. This chapter explores how computer vision and tracking systems capture player movements, and how to analyze this data to extract tactical insights, evaluate player performance, and inform game strategy.
Learning Objectives
By the end of this chapter, you will be able to:
- Understand player tracking data structures and sources
- Process and clean tracking data for analysis
- Calculate speed, acceleration, and movement metrics
- Analyze formations and player spacing
- Measure separation and route running quality
- Build visualizations of player movements
24.1 Tracking Data Fundamentals
24.1.1 Data Sources and Collection
Modern tracking systems capture player positions at high frequencies (typically 10-25 Hz), providing x,y coordinates for all players and the ball throughout each play.
import pandas as pd
import numpy as np
from typing import Dict, List, Tuple
from dataclasses import dataclass
@dataclass
class TrackingFrame:
"""Single frame of tracking data."""
game_id: str
play_id: str
frame_id: int
timestamp: float
player_id: str
team: str
x: float # Yard line (0-120)
y: float # Field width (0-53.3)
speed: float # yards/second
acceleration: float
direction: float # degrees (0-360)
orientation: float # player facing direction
class TrackingDataLoader:
"""Load and process tracking data."""
def __init__(self):
self.frame_rate = 10 # Hz
self.field_length = 120 # yards (including end zones)
self.field_width = 53.3 # yards
def load_play(self, filepath: str) -> pd.DataFrame:
"""Load tracking data for a single play."""
df = pd.read_csv(filepath)
# Standardize column names
df = df.rename(columns={
'x': 'x_pos',
'y': 'y_pos',
's': 'speed',
'a': 'acceleration',
'dir': 'direction',
'o': 'orientation'
})
# Ensure consistent ordering
df = df.sort_values(['frame_id', 'player_id'])
return df
def standardize_direction(self,
df: pd.DataFrame,
play_direction: str) -> pd.DataFrame:
"""Standardize coordinates so offense always moves left to right."""
if play_direction == 'left':
df['x_pos'] = self.field_length - df['x_pos']
df['y_pos'] = self.field_width - df['y_pos']
df['direction'] = (df['direction'] + 180) % 360
df['orientation'] = (df['orientation'] + 180) % 360
return df
24.1.2 Data Structure
TRACKING DATA SCHEMA
====================
Per-Frame Data:
- game_id: Unique game identifier
- play_id: Play within game
- frame_id: Frame number (1, 2, 3, ...)
- timestamp: Milliseconds from start
Player Data:
- player_id: NFL/NCAA ID
- jersey_number: Player number
- position: Position code
- team: Offense/Defense/Football
Location Data:
- x: Position along field (0-120 yards)
- y: Position across field (0-53.3 yards)
- speed: Instantaneous speed (yards/second)
- acceleration: Rate of speed change
- direction: Movement direction (degrees)
- orientation: Body facing direction (degrees)
Sample Rates:
- NFL Next Gen Stats: 10 Hz
- College tracking: varies (10-25 Hz)
- Broadcast tracking: 25-30 Hz
24.2 Movement Metrics
24.2.1 Speed and Acceleration
class MovementAnalyzer:
"""Analyze player movement from tracking data."""
def __init__(self, frame_rate: int = 10):
self.frame_rate = frame_rate
self.dt = 1.0 / frame_rate
def calculate_velocity(self, df: pd.DataFrame) -> pd.DataFrame:
"""Calculate velocity components."""
df = df.copy()
# Group by player
for player_id in df['player_id'].unique():
mask = df['player_id'] == player_id
# Calculate velocity from position changes
df.loc[mask, 'vx'] = df.loc[mask, 'x_pos'].diff() / self.dt
df.loc[mask, 'vy'] = df.loc[mask, 'y_pos'].diff() / self.dt
# Calculated speed
df['calc_speed'] = np.sqrt(df['vx']**2 + df['vy']**2)
return df
def calculate_acceleration(self, df: pd.DataFrame) -> pd.DataFrame:
"""Calculate acceleration from velocity."""
df = df.copy()
for player_id in df['player_id'].unique():
mask = df['player_id'] == player_id
df.loc[mask, 'ax'] = df.loc[mask, 'vx'].diff() / self.dt
df.loc[mask, 'ay'] = df.loc[mask, 'vy'].diff() / self.dt
df['calc_accel'] = np.sqrt(df['ax']**2 + df['ay']**2)
return df
def get_max_speed(self, df: pd.DataFrame) -> pd.DataFrame:
"""Get maximum speed for each player on play."""
return df.groupby('player_id')['speed'].max().reset_index()
def calculate_distance_traveled(self, df: pd.DataFrame) -> pd.DataFrame:
"""Calculate total distance traveled per player."""
df = df.copy()
results = []
for player_id in df['player_id'].unique():
player_df = df[df['player_id'] == player_id].sort_values('frame_id')
dx = player_df['x_pos'].diff()
dy = player_df['y_pos'].diff()
distance = np.sqrt(dx**2 + dy**2).sum()
results.append({
'player_id': player_id,
'distance_traveled': distance
})
return pd.DataFrame(results)
24.2.2 Route Analysis
class RouteAnalyzer:
"""Analyze receiver routes from tracking data."""
def __init__(self):
self.route_classifications = {
'go': self._is_go_route,
'slant': self._is_slant,
'out': self._is_out,
'in': self._is_in_route,
'curl': self._is_curl,
'corner': self._is_corner,
'post': self._is_post
}
def extract_route(self,
player_df: pd.DataFrame,
snap_frame: int) -> Dict:
"""Extract route characteristics for a receiver."""
# Filter to post-snap frames
route_df = player_df[player_df['frame_id'] >= snap_frame].copy()
if len(route_df) < 5:
return {'route_type': 'unknown', 'depth': 0}
# Starting position
start_x = route_df.iloc[0]['x_pos']
start_y = route_df.iloc[0]['y_pos']
# Deepest point
depth = route_df['x_pos'].max() - start_x
# Lateral movement
lateral = route_df['y_pos'].iloc[-1] - start_y
# Break point (max curvature)
break_frame = self._find_break_point(route_df)
return {
'start_x': start_x,
'start_y': start_y,
'depth': depth,
'lateral_movement': lateral,
'break_frame': break_frame,
'route_type': self._classify_route(route_df, depth, lateral)
}
def _find_break_point(self, df: pd.DataFrame) -> int:
"""Find frame where route direction changes most."""
if len(df) < 3:
return df['frame_id'].iloc[0]
directions = df['direction'].values
direction_changes = np.abs(np.diff(directions))
# Handle wraparound
direction_changes = np.minimum(direction_changes, 360 - direction_changes)
break_idx = np.argmax(direction_changes)
return df['frame_id'].iloc[break_idx]
def _classify_route(self,
df: pd.DataFrame,
depth: float,
lateral: float) -> str:
"""Classify route type based on path."""
if depth > 15 and abs(lateral) < 5:
return 'go'
elif depth < 8 and lateral > 5:
return 'out'
elif depth < 8 and lateral < -5:
return 'in'
elif depth > 10 and lateral > 8:
return 'corner'
elif depth > 10 and lateral < -8:
return 'post'
elif depth < 12 and abs(lateral) < 3:
return 'curl'
elif depth < 8 and 2 < abs(lateral) < 8:
return 'slant'
else:
return 'other'
def calculate_separation(self,
receiver_df: pd.DataFrame,
defender_df: pd.DataFrame) -> pd.DataFrame:
"""Calculate separation between receiver and nearest defender."""
# Merge on frame
merged = receiver_df.merge(
defender_df,
on='frame_id',
suffixes=('_rec', '_def')
)
# Calculate distance
merged['separation'] = np.sqrt(
(merged['x_pos_rec'] - merged['x_pos_def'])**2 +
(merged['y_pos_rec'] - merged['y_pos_def'])**2
)
return merged[['frame_id', 'separation']]
24.3 Formation Analysis
24.3.1 Pre-Snap Formations
class FormationAnalyzer:
"""Analyze offensive and defensive formations."""
def __init__(self):
self.los_margin = 1.0 # yards from LOS to consider on line
def identify_offensive_formation(self,
offense_df: pd.DataFrame,
snap_frame: int) -> Dict:
"""Identify offensive formation at snap."""
pre_snap = offense_df[offense_df['frame_id'] == snap_frame - 1]
# Find line of scrimmage (ball position)
ball_x = pre_snap[pre_snap['position'] == 'QB']['x_pos'].iloc[0]
# Count players in backfield
backfield = pre_snap[pre_snap['x_pos'] < ball_x - 3]
rb_count = len(backfield[backfield['position'].isin(['RB', 'FB'])])
# Count receivers
receivers = pre_snap[pre_snap['position'].isin(['WR', 'TE'])]
left_receivers = len(receivers[receivers['y_pos'] < 26.65])
right_receivers = len(receivers[receivers['y_pos'] > 26.65])
# Determine formation
if rb_count == 2:
backfield_type = 'I-Form' if self._is_i_formation(backfield) else 'Split Back'
elif rb_count == 1:
backfield_type = 'Single Back'
else:
backfield_type = 'Empty'
return {
'backfield': backfield_type,
'rb_count': rb_count,
'left_receivers': left_receivers,
'right_receivers': right_receivers,
'formation_strength': 'Left' if left_receivers > right_receivers else 'Right'
}
def _is_i_formation(self, backfield_df: pd.DataFrame) -> bool:
"""Check if backfield is in I-formation."""
if len(backfield_df) < 2:
return False
y_positions = backfield_df['y_pos'].values
return abs(y_positions[0] - y_positions[1]) < 2
def calculate_box_count(self,
defense_df: pd.DataFrame,
los_x: float) -> int:
"""Count defenders in the box."""
box_defenders = defense_df[
(defense_df['x_pos'] > los_x - 5) &
(defense_df['x_pos'] < los_x + 3) &
(defense_df['y_pos'] > 20) &
(defense_df['y_pos'] < 33)
]
return len(box_defenders)
def calculate_offensive_spacing(self,
offense_df: pd.DataFrame) -> Dict:
"""Calculate offensive spacing metrics."""
positions = offense_df[['x_pos', 'y_pos']].values
# Average distance between players
distances = []
for i in range(len(positions)):
for j in range(i+1, len(positions)):
dist = np.sqrt(
(positions[i,0] - positions[j,0])**2 +
(positions[i,1] - positions[j,1])**2
)
distances.append(dist)
return {
'avg_spacing': np.mean(distances),
'min_spacing': np.min(distances),
'max_spacing': np.max(distances),
'spread_width': offense_df['y_pos'].max() - offense_df['y_pos'].min()
}
24.4 Visualization
24.4.1 Animated Play Visualization
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle, Circle
from matplotlib.animation import FuncAnimation
class PlayVisualizer:
"""Visualize tracking data as animated plays."""
FIELD_LENGTH = 120
FIELD_WIDTH = 53.3
def __init__(self):
self.colors = {
'offense': '#e74c3c',
'defense': '#3498db',
'football': '#8B4513'
}
def create_field(self, ax: plt.Axes) -> plt.Axes:
"""Create football field visualization."""
# Field background
ax.set_facecolor('#228B22')
# Yard lines
for yard in range(0, 121, 10):
ax.axvline(x=yard, color='white', alpha=0.5, linewidth=1)
# Hash marks
for yard in range(0, 121, 1):
ax.plot([yard, yard], [22.9, 23.5], color='white', linewidth=0.5)
ax.plot([yard, yard], [29.8, 30.4], color='white', linewidth=0.5)
# End zones
ax.add_patch(Rectangle((0, 0), 10, self.FIELD_WIDTH,
color='#1a5c1a', alpha=0.5))
ax.add_patch(Rectangle((110, 0), 10, self.FIELD_WIDTH,
color='#1a5c1a', alpha=0.5))
# Field boundaries
ax.set_xlim(0, self.FIELD_LENGTH)
ax.set_ylim(0, self.FIELD_WIDTH)
ax.set_aspect('equal')
return ax
def plot_frame(self,
ax: plt.Axes,
frame_df: pd.DataFrame) -> List:
"""Plot single frame of tracking data."""
artists = []
for _, player in frame_df.iterrows():
color = self.colors.get(player['team'], 'gray')
# Player position
circle = Circle(
(player['x_pos'], player['y_pos']),
radius=0.8,
color=color,
alpha=0.8
)
ax.add_patch(circle)
artists.append(circle)
# Jersey number
text = ax.text(
player['x_pos'], player['y_pos'],
str(int(player.get('jersey_number', 0))),
ha='center', va='center',
fontsize=6, color='white', fontweight='bold'
)
artists.append(text)
return artists
def animate_play(self,
play_df: pd.DataFrame,
filepath: str = None) -> FuncAnimation:
"""Create animated visualization of a play."""
fig, ax = plt.subplots(figsize=(14, 7))
self.create_field(ax)
frames = play_df['frame_id'].unique()
def update(frame_num):
ax.clear()
self.create_field(ax)
frame_df = play_df[play_df['frame_id'] == frames[frame_num]]
self.plot_frame(ax, frame_df)
ax.set_title(f'Frame {frame_num}')
anim = FuncAnimation(
fig, update,
frames=len(frames),
interval=100,
blit=False
)
if filepath:
anim.save(filepath, writer='pillow', fps=10)
return anim
Summary
Computer vision and tracking data provide unprecedented insight into player movements and spatial relationships in football. Key applications include:
- Movement Analysis: Speed, acceleration, and distance metrics quantify player athleticism and effort
- Route Analysis: Detailed route tracking enables separation analysis and route classification
- Formation Recognition: Pre-snap formations can be automatically identified and categorized
- Tactical Insights: Spacing and positioning reveal strategic tendencies
The challenge lies in processing large volumes of high-frequency data and extracting meaningful football insights from raw coordinates.
Key Takeaways
- Tracking data captures x,y positions at 10-25 Hz for all players
- Speed and acceleration metrics derived from position changes
- Route analysis quantifies receiver skill and separation
- Formation analysis reveals tactical tendencies
- Visualization is essential for communicating spatial insights
Related Reading
Explore this topic in other books
AI Engineering Vision Transformers Basketball Analytics Computer Vision in Basketball Soccer Analytics Computer Vision for Soccer