mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
419 lines
16 KiB
Python
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())
|