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()