foot_video_stat / batch.py
simonlesaumon's picture
Upload folder using huggingface_hub
497fce0 verified
Raw
History Blame Contribute Delete
10.1 kB
import os
import sys
# Add parent directory to sys.path to enable package-level imports
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import glob
import sqlite3
import subprocess
import json
import argparse
import datetime
import traceback
import football_analytics.config as config
def parse_args():
parser = argparse.ArgumentParser(description="Football Analytics Batch Processor")
parser.add_argument("--input_dir", type=str, required=True, help="Directory containing raw football videos")
parser.add_argument("--output_dir", type=str, default=None, help="Directory for exports and DB")
parser.add_argument("--skip_frames", type=int, default=2, help="Skip frames ratio (default 2 for batch speed)")
parser.add_argument("--no_video", action="store_true", help="Skip writing output videos to save space and processing time")
parser.add_argument("--force", action="store_true", help="Force reprocessing of already processed videos")
return parser.parse_args()
def init_master_db(db_path):
"""
Initializes the master tracking and statistics database.
"""
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# 1. Processed videos tracking table
cursor.execute("""
CREATE TABLE IF NOT EXISTS processed_videos (
video_path TEXT PRIMARY KEY,
video_name TEXT,
processed_at TIMESTAMP,
status TEXT,
error_message TEXT
)
""")
# 2. Aggregated Match Summaries table
cursor.execute("""
CREATE TABLE IF NOT EXISTS batch_game_summaries (
video_name TEXT PRIMARY KEY,
possession_team_A REAL,
possession_team_B REAL,
total_distance_team_A_meters REAL,
total_distance_team_B_meters REAL,
passes_attempted_team_A INTEGER,
passes_completed_team_A INTEGER,
passes_attempted_team_B INTEGER,
passes_completed_team_B INTEGER
)
""")
# 3. Aggregated Player Summaries table
cursor.execute("""
CREATE TABLE IF NOT EXISTS batch_player_summaries (
video_name TEXT,
player_id INTEGER,
team_id INTEGER,
distance_covered_meters REAL,
possession_time_seconds REAL,
successful_passes INTEGER,
total_passes INTEGER,
pass_accuracy REAL,
average_x REAL,
average_y REAL,
PRIMARY KEY (video_name, player_id)
)
""")
conn.commit()
conn.close()
def log_video_status(db_path, video_path, status, error_message=None):
"""
Updates the status of a video in the tracking table.
"""
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
video_name = os.path.basename(video_path)
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cursor.execute("""
INSERT OR REPLACE INTO processed_videos (video_path, video_name, processed_at, status, error_message)
VALUES (?, ?, ?, ?, ?)
""", (video_path, video_name, now, status, error_message))
conn.commit()
conn.close()
def check_video_processed(db_path, video_path):
"""
Returns True if the video was already successfully processed.
"""
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute("SELECT status FROM processed_videos WHERE video_path = ?", (video_path,))
row = cursor.fetchone()
conn.close()
if row and row[0] == "success":
return True
return False
def aggregate_stats_to_db(db_path, video_path, summary_json_path):
"""
Extracts analytics from a video's summary.json and saves them in the master database.
"""
if not os.path.exists(summary_json_path):
print(f"[Batch] Warning: Summary JSON file not found at {summary_json_path}. Cannot aggregate stats.")
return
with open(summary_json_path, 'r') as f:
summary_data = json.load(f)
video_name = os.path.basename(video_path)
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# 1. Insert game summary
ms = summary_data["match_statistics"]
cursor.execute("""
INSERT OR REPLACE INTO batch_game_summaries (
video_name, possession_team_A, possession_team_B,
total_distance_team_A_meters, total_distance_team_B_meters,
passes_attempted_team_A, passes_completed_team_A,
passes_attempted_team_B, passes_completed_team_B
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
video_name,
ms.get("possession_percent_team_A", 50.0),
ms.get("possession_percent_team_B", 50.0),
ms.get("total_distance_team_A_meters", 0.0),
ms.get("total_distance_team_B_meters", 0.0),
ms.get("passes_attempted_team_A", 0),
ms.get("passes_completed_team_A", 0),
ms.get("passes_attempted_team_B", 0),
ms.get("passes_completed_team_B", 0)
))
# 2. Insert player summaries
for p in summary_data.get("players", []):
cursor.execute("""
INSERT OR REPLACE INTO batch_player_summaries (
video_name, player_id, team_id, distance_covered_meters,
possession_time_seconds, successful_passes, total_passes,
pass_accuracy, average_x, average_y
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
video_name,
p.get("player_id"),
p.get("team_id"),
p.get("distance_covered_meters", 0.0),
p.get("possession_time_seconds", 0.0),
p.get("successful_passes", 0),
p.get("total_passes", 0),
p.get("pass_accuracy", 0.0),
p.get("average_x", 0.0),
p.get("average_y", 0.0)
))
conn.commit()
conn.close()
print(f"[Batch] Aggregated stats for {video_name} into master database.")
def main():
args = parse_args()
if not os.path.exists(args.input_dir):
print(f"Error: Input directory '{args.input_dir}' does not exist.")
return
output_dir = args.output_dir or config.OUTPUT_DIR
os.makedirs(output_dir, exist_ok=True)
master_db_path = os.path.join(output_dir, "batch_analytics.sqlite")
init_master_db(master_db_path)
# Supported video formats
video_extensions = ["*.mp4", "*.avi", "*.mkv", "*.mov", "*.MP4", "*.MOV"]
video_files = []
for ext in video_extensions:
video_files.extend(glob.glob(os.path.join(args.input_dir, ext)))
# Remove duplicates if any
video_files = list(set(video_files))
print("\n" + "="*60)
print(f"FOOTBALL ANALYTICS BATCH PROCESSOR")
print(f"Input Directory: {args.input_dir}")
print(f"Output Directory: {output_dir}")
print(f"Master Database: {master_db_path}")
print(f"Videos Found: {len(video_files)}")
print("="*60 + "\n")
if not video_files:
print("No video files found matching typical extensions.")
return
for i, video_path in enumerate(video_files):
video_name = os.path.basename(video_path)
video_name_no_ext = os.path.splitext(video_name)[0]
print(f"\n[{i+1}/{len(video_files)}] Processing: {video_name}")
# Check tracking to skip if already done
if not args.force and check_video_processed(master_db_path, video_path):
print(f" -> Skipping. Already successfully processed. (Use --force to reprocess)")
continue
# Create output subdirectory for this specific video
video_out_dir = os.path.join(output_dir, video_name_no_ext)
os.makedirs(video_out_dir, exist_ok=True)
# Mark running
log_video_status(master_db_path, video_path, "running")
# Build command line for subprocess
# Running via subprocess guarantees GPU memory is cleared completely between video runs
cmd = [
"python3", "pipeline.py",
"--video", video_path,
"--output_dir", video_out_dir,
"--skip_frames", str(args.skip_frames)
]
if args.no_video:
cmd.append("--no_video")
print(f" -> Launching subprocess pipeline...")
print(f" -> Command: {' '.join(cmd)}")
try:
# Run the pipeline
result = subprocess.run(
cmd,
cwd=config.BASE_DIR,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# Print last few lines of output
print(" -> Pipeline finished successfully.")
# Log success
log_video_status(master_db_path, video_path, "success")
# Aggregate stats to master DB
summary_path = os.path.join(video_out_dir, "summary.json")
aggregate_stats_to_db(master_db_path, video_path, summary_path)
except subprocess.CalledProcessError as e:
print(f" -> ERROR: Pipeline subprocess failed for {video_name}.")
print("--- Subprocess Error Stdout ---")
print(e.stdout[-1000:] if e.stdout else "No stdout output")
print("--- Subprocess Error Stderr ---")
print(e.stderr[-1000:] if e.stderr else "No stderr output")
log_video_status(master_db_path, video_path, "failed", error_message=e.stderr)
except Exception as e:
print(f" -> ERROR: Unexpected failure for {video_name}: {e}")
log_video_status(master_db_path, video_path, "failed", error_message=str(e))
print("\n" + "="*60)
print("BATCH PROCESSING TASK COMPLETE!")
print(f"Check results database at: {master_db_path}")
print("="*60 + "\n")
if __name__ == "__main__":
main()