Files
LEDMatrix/test/analyze_broadcast_logos.py

419 lines
16 KiB
Python

#!/usr/bin/env python3
"""
Broadcast Logo Analyzer
This script analyzes broadcast channel logos to ensure we have proper logos
for every game and identifies missing or problematic logos that might show
as white boxes.
IMPORTANT: This script must be run on the Raspberry Pi where the LEDMatrix
project is located, as it needs to access the actual logo files in the
assets/broadcast_logos/ directory.
Usage (on Raspberry Pi):
python test/analyze_broadcast_logos.py
Features:
- Checks all broadcast logos referenced in BROADCAST_LOGO_MAP
- Validates logo file existence and integrity
- Analyzes logo dimensions and transparency
- Identifies potential white box issues
- Provides recommendations for missing logos
- Generates a detailed report
"""
import os
import sys
import json
from pathlib import Path
from typing import Dict, List, Set, Tuple, Optional
from PIL import Image, ImageStat
import logging
# Add the project root to the path so we can import from src
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
# Define the broadcast logo map directly (copied from odds_ticker_manager.py)
BROADCAST_LOGO_MAP = {
"ACC Network": "accn",
"ACCN": "accn",
"ABC": "abc",
"BTN": "btn",
"CBS": "cbs",
"CBSSN": "cbssn",
"CBS Sports Network": "cbssn",
"ESPN": "espn",
"ESPN2": "espn2",
"ESPN3": "espn3",
"ESPNU": "espnu",
"ESPNEWS": "espn",
"ESPN+": "espn",
"ESPN Plus": "espn",
"FOX": "fox",
"FS1": "fs1",
"FS2": "fs2",
"MLBN": "mlbn",
"MLB Network": "mlbn",
"MLB.TV": "mlbn",
"NBC": "nbc",
"NFLN": "nfln",
"NFL Network": "nfln",
"PAC12": "pac12n",
"Pac-12 Network": "pac12n",
"SECN": "espn-sec-us",
"TBS": "tbs",
"TNT": "tnt",
"truTV": "tru",
"Peacock": "nbc",
"Paramount+": "cbs",
"Hulu": "espn",
"Disney+": "espn",
"Apple TV+": "nbc",
# Regional sports networks
"MASN": "cbs",
"MASN2": "cbs",
"MAS+": "cbs",
"SportsNet": "nbc",
"FanDuel SN": "fox",
"FanDuel SN DET": "fox",
"FanDuel SN FL": "fox",
"SportsNet PIT": "nbc",
"Padres.TV": "espn",
"CLEGuardians.TV": "espn"
}
# Set up logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class BroadcastLogoAnalyzer:
"""Analyzes broadcast channel logos for completeness and quality."""
def __init__(self, project_root: Path):
self.project_root = project_root
self.broadcast_logos_dir = project_root / "assets" / "broadcast_logos"
self.results = {
'total_mappings': len(BROADCAST_LOGO_MAP),
'existing_logos': [],
'missing_logos': [],
'problematic_logos': [],
'recommendations': []
}
def analyze_all_logos(self) -> Dict:
"""Perform comprehensive analysis of all broadcast logos."""
logger.info("Starting broadcast logo analysis...")
# Get all logo files that exist
existing_files = self._get_existing_logo_files()
logger.info(f"Found {len(existing_files)} existing logo files")
# Check each mapping in BROADCAST_LOGO_MAP
for broadcast_name, logo_filename in BROADCAST_LOGO_MAP.items():
self._analyze_logo_mapping(broadcast_name, logo_filename, existing_files)
# Check for orphaned logo files (files that exist but aren't mapped)
self._check_orphaned_logos(existing_files)
# Generate recommendations
self._generate_recommendations()
return self.results
def _get_existing_logo_files(self) -> Set[str]:
"""Get all existing logo files in the broadcast_logos directory."""
existing_files = set()
if not self.broadcast_logos_dir.exists():
logger.warning(f"Broadcast logos directory does not exist: {self.broadcast_logos_dir}")
return existing_files
for file_path in self.broadcast_logos_dir.iterdir():
if file_path.is_file() and file_path.suffix.lower() in ['.png', '.jpg', '.jpeg']:
existing_files.add(file_path.stem) # filename without extension
return existing_files
def _analyze_logo_mapping(self, broadcast_name: str, logo_filename: str, existing_files: Set[str]):
"""Analyze a single logo mapping."""
logo_path = self.broadcast_logos_dir / f"{logo_filename}.png"
if logo_filename not in existing_files:
self.results['missing_logos'].append({
'broadcast_name': broadcast_name,
'logo_filename': logo_filename,
'expected_path': str(logo_path)
})
logger.warning(f"Missing logo: {broadcast_name} -> {logo_filename}.png")
return
# Logo exists, analyze its quality
try:
analysis = self._analyze_logo_quality(logo_path, broadcast_name, logo_filename)
if analysis['is_problematic']:
self.results['problematic_logos'].append(analysis)
else:
self.results['existing_logos'].append(analysis)
except Exception as e:
logger.error(f"Error analyzing logo {logo_path}: {e}")
self.results['problematic_logos'].append({
'broadcast_name': broadcast_name,
'logo_filename': logo_filename,
'path': str(logo_path),
'error': str(e),
'is_problematic': True
})
def _analyze_logo_quality(self, logo_path: Path, broadcast_name: str, logo_filename: str) -> Dict:
"""Analyze the quality of a logo file."""
try:
with Image.open(logo_path) as img:
# Basic image info
width, height = img.size
mode = img.mode
# Convert to RGBA for analysis if needed
if mode != 'RGBA':
img_rgba = img.convert('RGBA')
else:
img_rgba = img
# Analyze for potential white box issues
analysis = {
'broadcast_name': broadcast_name,
'logo_filename': logo_filename,
'path': str(logo_path),
'dimensions': (width, height),
'mode': mode,
'file_size': logo_path.stat().st_size,
'is_problematic': False,
'issues': [],
'recommendations': []
}
# Check for white box issues
self._check_white_box_issues(img_rgba, analysis)
# Check dimensions
self._check_dimensions(width, height, analysis)
# Check transparency
self._check_transparency(img_rgba, analysis)
# Check if image is mostly empty/white
self._check_content_density(img_rgba, analysis)
return analysis
except Exception as e:
raise Exception(f"Failed to analyze image: {e}")
def _check_white_box_issues(self, img: Image.Image, analysis: Dict):
"""Check for potential white box issues."""
# Get image statistics
stat = ImageStat.Stat(img)
# Check if image is mostly white
if img.mode == 'RGBA':
# For RGBA, check RGB channels
r_mean, g_mean, b_mean = stat.mean[:3]
if r_mean > 240 and g_mean > 240 and b_mean > 240:
analysis['issues'].append("Image appears to be mostly white")
analysis['is_problematic'] = True
# Check for completely transparent images
if img.mode == 'RGBA':
alpha_channel = img.split()[3]
alpha_stat = ImageStat.Stat(alpha_channel)
if alpha_stat.mean[0] < 10: # Very low alpha
analysis['issues'].append("Image is mostly transparent")
analysis['is_problematic'] = True
def _check_dimensions(self, width: int, height: int, analysis: Dict):
"""Check if dimensions are reasonable."""
if width < 16 or height < 16:
analysis['issues'].append(f"Very small dimensions: {width}x{height}")
analysis['is_problematic'] = True
analysis['recommendations'].append("Consider using a higher resolution logo")
if width > 512 or height > 512:
analysis['issues'].append(f"Very large dimensions: {width}x{height}")
analysis['recommendations'].append("Consider optimizing logo size for better performance")
# Check aspect ratio
aspect_ratio = width / height
if aspect_ratio > 4 or aspect_ratio < 0.25:
analysis['issues'].append(f"Extreme aspect ratio: {aspect_ratio:.2f}")
analysis['recommendations'].append("Consider using a more square logo")
def _check_transparency(self, img: Image.Image, analysis: Dict):
"""Check transparency handling."""
if img.mode == 'RGBA':
# Check if there's any transparency
alpha_channel = img.split()[3]
alpha_data = list(alpha_channel.getdata())
min_alpha = min(alpha_data)
max_alpha = max(alpha_data)
if min_alpha < 255:
analysis['recommendations'].append("Logo has transparency - ensure proper background handling")
if max_alpha < 128:
analysis['issues'].append("Logo is very transparent")
analysis['is_problematic'] = True
def _check_content_density(self, img: Image.Image, analysis: Dict):
"""Check if the image has sufficient content."""
# Convert to grayscale for analysis
gray = img.convert('L')
# Count non-white pixels (assuming white background)
pixels = list(gray.getdata())
non_white_pixels = sum(1 for p in pixels if p < 240)
total_pixels = len(pixels)
content_ratio = non_white_pixels / total_pixels
if content_ratio < 0.05: # Less than 5% content
analysis['issues'].append(f"Very low content density: {content_ratio:.1%}")
analysis['is_problematic'] = True
analysis['recommendations'].append("Logo may appear as a white box - check content")
def _check_orphaned_logos(self, existing_files: Set[str]):
"""Check for logo files that exist but aren't mapped."""
mapped_filenames = set(BROADCAST_LOGO_MAP.values())
orphaned_files = existing_files - mapped_filenames
if orphaned_files:
self.results['orphaned_logos'] = list(orphaned_files)
logger.info(f"Found {len(orphaned_files)} orphaned logo files: {orphaned_files}")
def _generate_recommendations(self):
"""Generate overall recommendations."""
recommendations = []
if self.results['missing_logos']:
recommendations.append(f"Add {len(self.results['missing_logos'])} missing logo files")
if self.results['problematic_logos']:
recommendations.append(f"Fix {len(self.results['problematic_logos'])} problematic logos")
if 'orphaned_logos' in self.results:
recommendations.append(f"Consider mapping {len(self.results['orphaned_logos'])} orphaned logo files")
# General recommendations
recommendations.extend([
"Ensure all logos are PNG format with transparency support",
"Use consistent dimensions (preferably 64x64 or 128x128 pixels)",
"Test logos on the actual LED matrix display",
"Consider creating fallback logos for missing channels"
])
self.results['recommendations'] = recommendations
def print_report(self):
"""Print a detailed analysis report."""
print("\n" + "="*80)
print("BROADCAST LOGO ANALYSIS REPORT")
print("="*80)
print(f"\nSUMMARY:")
print(f" Total broadcast mappings: {self.results['total_mappings']}")
print(f" Existing logos: {len(self.results['existing_logos'])}")
print(f" Missing logos: {len(self.results['missing_logos'])}")
print(f" Problematic logos: {len(self.results['problematic_logos'])}")
if 'orphaned_logos' in self.results:
print(f" Orphaned logos: {len(self.results['orphaned_logos'])}")
# Missing logos
if self.results['missing_logos']:
print(f"\nMISSING LOGOS ({len(self.results['missing_logos'])}):")
print("-" * 50)
for missing in self.results['missing_logos']:
print(f" {missing['broadcast_name']} -> {missing['logo_filename']}.png")
print(f" Expected: {missing['expected_path']}")
# Problematic logos
if self.results['problematic_logos']:
print(f"\nPROBLEMATIC LOGOS ({len(self.results['problematic_logos'])}):")
print("-" * 50)
for problematic in self.results['problematic_logos']:
print(f" {problematic['broadcast_name']} -> {problematic['logo_filename']}")
if 'error' in problematic:
print(f" Error: {problematic['error']}")
if 'issues' in problematic:
for issue in problematic['issues']:
print(f" Issue: {issue}")
if 'recommendations' in problematic:
for rec in problematic['recommendations']:
print(f" Recommendation: {rec}")
# Orphaned logos
if 'orphaned_logos' in self.results and self.results['orphaned_logos']:
print(f"\nORPHANED LOGOS ({len(self.results['orphaned_logos'])}):")
print("-" * 50)
for orphaned in self.results['orphaned_logos']:
print(f" {orphaned}.png (not mapped in BROADCAST_LOGO_MAP)")
# Recommendations
if self.results['recommendations']:
print(f"\nRECOMMENDATIONS:")
print("-" * 50)
for i, rec in enumerate(self.results['recommendations'], 1):
print(f" {i}. {rec}")
print("\n" + "="*80)
def save_report(self, output_file: str = "broadcast_logo_analysis.json"):
"""Save the analysis results to a JSON file."""
output_path = self.project_root / "test" / output_file
with open(output_path, 'w') as f:
json.dump(self.results, f, indent=2)
logger.info(f"Analysis report saved to: {output_path}")
def main():
"""Main function to run the broadcast logo analysis."""
print("Broadcast Logo Analyzer")
print("=" * 50)
# Check if we're in the right directory structure
if not (project_root / "assets" / "broadcast_logos").exists():
print("ERROR: This script must be run from the LEDMatrix project root directory")
print(f"Expected directory structure: {project_root}/assets/broadcast_logos/")
print("Please run this script on the Raspberry Pi where the LEDMatrix project is located.")
print("\nTo test the script logic locally, you can copy some logo files to the expected location.")
return 1
# Initialize analyzer
analyzer = BroadcastLogoAnalyzer(project_root)
# Run analysis
try:
results = analyzer.analyze_all_logos()
# Print report
analyzer.print_report()
# Save report
analyzer.save_report()
# Return exit code based on issues found
total_issues = len(results['missing_logos']) + len(results['problematic_logos'])
if total_issues > 0:
print(f"\n⚠️ Found {total_issues} issues that need attention!")
return 1
else:
print(f"\n✅ All broadcast logos are in good condition!")
return 0
except Exception as e:
logger.error(f"Analysis failed: {e}")
return 1
if __name__ == "__main__":
exit(main())