Clean push: Removed heavy files & added only latest snapshot

This commit is contained in:
2025-07-26 05:16:12 +05:30
commit acf84e8767
250 changed files with 58564 additions and 0 deletions

203
qt_app_pyside1/finale/UI.py Normal file
View File

@@ -0,0 +1,203 @@
"""
Finale UI - Main Entry Point
Modern traffic monitoring interface entry point.
"""
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont, QPalette, QColor
import sys
import os
from pathlib import Path
# Import finale components
try:
# Try relative imports first (when running as a package)
from .main_window import FinaleMainWindow
from .splash import FinaleSplashScreen
from .styles import FinaleStyles, MaterialColors
from .icons import FinaleIcons
except ImportError:
# Fallback to direct imports (when running as script)
try:
from main_window import FinaleMainWindow
from splash import FinaleSplashScreen
from styles import FinaleStyles, MaterialColors
from icons import FinaleIcons
except ImportError:
print('Error importing main components')
# Add Qt message handler from original main.py
def qt_message_handler(mode, context, message):
print(f"Qt Message: {message} (Mode: {mode})")
# Install custom handler for Qt messages
from PySide6.QtCore import Qt
if hasattr(Qt, 'qInstallMessageHandler'):
Qt.qInstallMessageHandler(qt_message_handler)
class FinaleUI:
"""
Main Finale UI application class.
Handles application initialization, theme setup, and window management.
"""
def __init__(self):
self.app = None
self.main_window = None
self.splash = None
def initialize_application(self, sys_argv=None):
"""
Initialize the QApplication with proper settings.
Args:
sys_argv: System arguments (defaults to sys.argv)
"""
if sys_argv is None:
sys_argv = sys.argv
# Create or get existing application instance
self.app = QApplication.instance() or QApplication(sys_argv)
# Set application properties
self.app.setApplicationName("Finale Traffic Monitoring")
self.app.setApplicationVersion("1.0.0")
self.app.setOrganizationName("Finale Systems")
self.app.setOrganizationDomain("finale.traffic")
# Set application icon
self.app.setWindowIcon(FinaleIcons.get_icon("traffic_monitoring"))
# Enable high DPI scaling
self.app.setAttribute(Qt.AA_EnableHighDpiScaling, True)
self.app.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
# Set font
self.setup_fonts()
# Set global theme
self.setup_theme()
return self.app
def setup_fonts(self):
"""Setup application fonts"""
# Set default font
font = QFont("Segoe UI", 9)
font.setHintingPreference(QFont.PreferDefaultHinting)
self.app.setFont(font)
def setup_theme(self):
"""Setup global application theme"""
# Apply dark theme by default
MaterialColors.apply_dark_theme()
# Set global stylesheet
self.app.setStyleSheet(FinaleStyles.get_global_style())
def show_splash_screen(self):
"""Show splash screen during initialization"""
try:
self.splash = FinaleSplashScreen()
self.splash.show()
# Process events to show splash
self.app.processEvents()
return self.splash
except Exception as e:
print(f"Could not show splash screen: {e}")
return None
def create_main_window(self):
"""Create and initialize the main window"""
try:
self.main_window = FinaleMainWindow()
return self.main_window
except Exception as e:
print(f"Error creating main window: {e}")
raise
def run(self, show_splash=True):
"""
Run the complete Finale UI application.
Args:
show_splash: Whether to show splash screen
Returns:
Application exit code
"""
try:
# Initialize application
if not self.app:
self.initialize_application()
# Show splash screen
if show_splash:
splash = self.show_splash_screen()
if splash:
splash.update_progress(20, "Initializing UI components...")
self.app.processEvents()
# Create main window
if splash:
splash.update_progress(50, "Loading detection models...")
self.app.processEvents()
self.main_window = self.create_main_window()
if splash:
splash.update_progress(80, "Connecting to backend...")
self.app.processEvents()
# Finish splash and show main window
if splash:
splash.update_progress(100, "Ready!")
self.app.processEvents()
splash.finish(self.main_window)
# Show main window
self.main_window.show()
# Start event loop
return self.app.exec()
except Exception as e:
print(f"❌ Error running Finale UI: {e}")
import traceback
traceback.print_exc()
return 1
def create_finale_app(sys_argv=None):
"""
Create and return a Finale UI application instance.
Args:
sys_argv: System arguments
Returns:
FinaleUI instance
"""
finale_ui = FinaleUI()
finale_ui.initialize_application(sys_argv)
return finale_ui
def run_finale_ui(sys_argv=None, show_splash=True):
"""
Convenience function to run the Finale UI.
Args:
sys_argv: System arguments
show_splash: Whether to show splash screen
Returns:
Application exit code
"""
finale_ui = create_finale_app(sys_argv)
return finale_ui.run(show_splash)
# Main execution
if __name__ == "__main__":
exit_code = run_finale_ui()
sys.exit(exit_code)

View File

@@ -0,0 +1 @@
# Finale module for traffic monitoring system

View File

@@ -0,0 +1,432 @@
"""
Icon Management System
=====================
Comprehensive icon system with SVG icons, Material Design icons,
and utility functions for the Traffic Monitoring Application.
Features:
- Material Design icon set
- SVG icon generation
- Icon theming and colorization
- Size variants and scaling
- Custom icon registration
"""
from PySide6.QtGui import QIcon, QPixmap, QPainter, QColor, QBrush, QPen
from PySide6.QtCore import Qt, QSize
from PySide6.QtSvg import QSvgRenderer
from typing import Dict, Optional, Tuple
import base64
from io import BytesIO
class IconTheme:
"""Icon theme management"""
# Icon colors for dark theme
PRIMARY = "#FFFFFF"
SECONDARY = "#B0B0B0"
ACCENT = "#00BCD4"
SUCCESS = "#4CAF50"
WARNING = "#FF9800"
ERROR = "#F44336"
INFO = "#2196F3"
class SVGIcons:
"""Collection of SVG icons as base64 encoded strings"""
# Navigation icons
HOME = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2l7 7v11h-4v-7h-6v7H5V9l7-7z"/>
</svg>
"""
PLAY = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z"/>
</svg>
"""
PAUSE = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/>
</svg>
"""
STOP = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M6 6h12v12H6z"/>
</svg>
"""
RECORD = """
<svg viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="8"/>
</svg>
"""
# Detection and monitoring icons
CAMERA = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 8.8c-2.1 0-3.8 1.7-3.8 3.8s1.7 3.8 3.8 3.8 3.8-1.7 3.8-3.8-1.7-3.8-3.8-3.8z"/>
<path d="M21 7h-3.4l-1.9-2.6c-.4-.5-.9-.8-1.6-.8H9.9c-.7 0-1.2.3-1.6.8L6.4 7H3c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2z"/>
</svg>
"""
MONITOR = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M21 3H3c-1.1 0-2 .9-2 2v11c0 1.1.9 2 2 2h6l-2 3v1h8v-1l-2-3h6c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 11H3V5h18v9z"/>
</svg>
"""
TRAFFIC_LIGHT = """
<svg viewBox="0 0 24 24" fill="currentColor">
<rect x="8" y="2" width="8" height="20" rx="4" stroke="currentColor" stroke-width="2" fill="none"/>
<circle cx="12" cy="7" r="2" fill="#F44336"/>
<circle cx="12" cy="12" r="2" fill="#FF9800"/>
<circle cx="12" cy="17" r="2" fill="#4CAF50"/>
</svg>
"""
VIOLATION = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2L1 21h22L12 2zm0 3.99L19.53 19H4.47L12 5.99zM11 16h2v2h-2v-2zm0-6h2v4h-2v-4z"/>
</svg>
"""
# Analytics and statistics icons
CHART_BAR = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M5 9v6h4V9H5zm6-4v10h4V5h-4zm6 6v4h4v-4h-4z"/>
</svg>
"""
CHART_LINE = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M3.5 18.49l6-6.01 4 4L22 6.92l-1.41-1.41-7.09 7.97-4-4L3 16.99l.5 1.5z"/>
</svg>
"""
CHART_PIE = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M11 2v20c-5.07-.5-9-4.79-9-10s3.93-9.5 9-10zm2.03 0v8.99H22c-.47-4.74-4.24-8.52-8.97-8.99zm0 11.01V22c4.74-.47 8.5-4.25 8.97-8.99h-8.97z"/>
</svg>
"""
DASHBOARD = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/>
</svg>
"""
# System and settings icons
SETTINGS = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
</svg>
"""
EXPORT = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2L8 8h3v8h2V8h3l-4-6zm7 7h-2v10H7V9H5v10c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V9z"/>
</svg>
"""
IMPORT = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 22L8 16h3V8h2v8h3l-4 6zm7-15h-2V5H7v2H5V5c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2v2z"/>
</svg>
"""
SAVE = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V6h10v3z"/>
</svg>
"""
# Status and alert icons
CHECK_CIRCLE = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
"""
WARNING_CIRCLE = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
"""
ERROR_CIRCLE = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/>
</svg>
"""
INFO_CIRCLE = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
</svg>
"""
# Action icons
REFRESH = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
</svg>
"""
DELETE = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
</svg>
"""
EDIT = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>
</svg>
"""
FILTER = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
</svg>
"""
SEARCH = """
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
</svg>
"""
class IconManager:
"""Manages icons for the application"""
def __init__(self):
self._icon_cache: Dict[str, QIcon] = {}
self.theme = IconTheme()
def get_icon(self, name: str, color: str = IconTheme.PRIMARY, size: int = 24) -> QIcon:
"""Get an icon by name with specified color and size"""
cache_key = f"{name}_{color}_{size}"
if cache_key in self._icon_cache:
return self._icon_cache[cache_key]
# Get SVG content
svg_content = getattr(SVGIcons, name.upper(), None)
if not svg_content:
return QIcon() # Return empty icon if not found
# Replace currentColor with specified color
svg_content = svg_content.replace('currentColor', color)
# Create icon from SVG
icon = self._create_icon_from_svg(svg_content, size)
self._icon_cache[cache_key] = icon
return icon
def _create_icon_from_svg(self, svg_content: str, size: int) -> QIcon:
"""Create QIcon from SVG content"""
# Create QSvgRenderer from SVG content
svg_bytes = svg_content.encode('utf-8')
renderer = QSvgRenderer(svg_bytes)
# Create pixmap
pixmap = QPixmap(size, size)
pixmap.fill(Qt.transparent)
# Paint SVG onto pixmap
painter = QPainter(pixmap)
renderer.render(painter)
painter.end()
return QIcon(pixmap)
def get_status_icon(self, status: str, size: int = 16) -> QIcon:
"""Get icon for specific status"""
status_map = {
'success': ('CHECK_CIRCLE', IconTheme.SUCCESS),
'warning': ('WARNING_CIRCLE', IconTheme.WARNING),
'error': ('ERROR_CIRCLE', IconTheme.ERROR),
'info': ('INFO_CIRCLE', IconTheme.INFO),
'violation': ('VIOLATION', IconTheme.ERROR),
'active': ('PLAY', IconTheme.SUCCESS),
'inactive': ('PAUSE', IconTheme.SECONDARY),
'recording': ('RECORD', IconTheme.ERROR)
}
icon_name, color = status_map.get(status, ('INFO_CIRCLE', IconTheme.INFO))
return self.get_icon(icon_name, color, size)
def get_action_icon(self, action: str, size: int = 20) -> QIcon:
"""Get icon for specific action"""
action_map = {
'play': 'PLAY',
'pause': 'PAUSE',
'stop': 'STOP',
'record': 'RECORD',
'settings': 'SETTINGS',
'export': 'EXPORT',
'import': 'IMPORT',
'save': 'SAVE',
'refresh': 'REFRESH',
'delete': 'DELETE',
'edit': 'EDIT',
'filter': 'FILTER',
'search': 'SEARCH'
}
icon_name = action_map.get(action, 'INFO_CIRCLE')
return self.get_icon(icon_name, IconTheme.PRIMARY, size)
def get_navigation_icon(self, view: str, size: int = 24) -> QIcon:
"""Get icon for navigation views"""
nav_map = {
'home': 'HOME',
'detection': 'CAMERA',
'violations': 'VIOLATION',
'analytics': 'DASHBOARD',
'export': 'EXPORT',
'monitor': 'MONITOR',
'chart': 'CHART_BAR'
}
icon_name = nav_map.get(view, 'HOME')
return self.get_icon(icon_name, IconTheme.ACCENT, size)
def create_colored_icon(self, base_icon: str, color: str, size: int = 24) -> QIcon:
"""Create a colored version of an icon"""
return self.get_icon(base_icon, color, size)
def set_theme_color(self, color: str):
"""Set the theme accent color"""
self.theme.ACCENT = color
# Clear cache to regenerate icons with new color
self._icon_cache.clear()
# Global icon manager instance
icon_manager = IconManager()
# Convenience functions
def get_icon(name: str, color: str = IconTheme.PRIMARY, size: int = 24) -> QIcon:
"""Get an icon - convenience function"""
return icon_manager.get_icon(name, color, size)
def get_status_icon(status: str, size: int = 16) -> QIcon:
"""Get status icon - convenience function"""
return icon_manager.get_status_icon(status, size)
def get_action_icon(action: str, size: int = 20) -> QIcon:
"""Get action icon - convenience function"""
return icon_manager.get_action_icon(action, size)
def get_navigation_icon(view: str, size: int = 24) -> QIcon:
"""Get navigation icon - convenience function"""
return icon_manager.get_navigation_icon(view, size)
# Common icon sets for easy access
class CommonIcons:
"""Commonly used icon combinations"""
@staticmethod
def toolbar_icons() -> Dict[str, QIcon]:
"""Get all toolbar icons"""
return {
'play': get_action_icon('play'),
'pause': get_action_icon('pause'),
'stop': get_action_icon('stop'),
'record': get_action_icon('record'),
'settings': get_action_icon('settings'),
'export': get_action_icon('export'),
'refresh': get_action_icon('refresh')
}
@staticmethod
def status_icons() -> Dict[str, QIcon]:
"""Get all status icons"""
return {
'success': get_status_icon('success'),
'warning': get_status_icon('warning'),
'error': get_status_icon('error'),
'info': get_status_icon('info'),
'violation': get_status_icon('violation'),
'active': get_status_icon('active'),
'inactive': get_status_icon('inactive'),
'recording': get_status_icon('recording')
}
@staticmethod
def navigation_icons() -> Dict[str, QIcon]:
"""Get all navigation icons"""
return {
'detection': get_navigation_icon('detection'),
'violations': get_navigation_icon('violations'),
'analytics': get_navigation_icon('analytics'),
'export': get_navigation_icon('export'),
'monitor': get_navigation_icon('monitor')
}
# Traffic light specific icons
def create_traffic_light_icon(red_on: bool = False, yellow_on: bool = False, green_on: bool = False, size: int = 32) -> QIcon:
"""Create a traffic light icon with specific lights on/off"""
svg_template = f"""
<svg viewBox="0 0 24 24" width="{size}" height="{size}">
<rect x="8" y="2" width="8" height="20" rx="4" stroke="#424242" stroke-width="2" fill="#2C2C2C"/>
<circle cx="12" cy="7" r="2" fill="{'#F44336' if red_on else '#5D4037'}"/>
<circle cx="12" cy="12" r="2" fill="{'#FF9800' if yellow_on else '#5D4037'}"/>
<circle cx="12" cy="17" r="2" fill="{'#4CAF50' if green_on else '#5D4037'}"/>
</svg>
"""
svg_bytes = svg_template.encode('utf-8')
renderer = QSvgRenderer(svg_bytes)
pixmap = QPixmap(size, size)
pixmap.fill(Qt.transparent)
painter = QPainter(pixmap)
renderer.render(painter)
painter.end()
return QIcon(pixmap)
# New FinaleIcons class to wrap the existing functionality
class FinaleIcons:
"""
Wrapper class for icon management to maintain compatibility
with existing code that references FinaleIcons.get_icon() etc.
"""
@staticmethod
def get_icon(name: str, color: str = IconTheme.PRIMARY, size: int = 24) -> QIcon:
"""Get an icon by name"""
return get_icon(name, color, size)
@staticmethod
def get_status_icon(status: str, size: int = 16) -> QIcon:
"""Get a status icon"""
return get_status_icon(status, size)
@staticmethod
def get_action_icon(action: str, size: int = 20) -> QIcon:
"""Get an action icon"""
return get_action_icon(action, size)
@staticmethod
def get_navigation_icon(view: str, size: int = 24) -> QIcon:
"""Get a navigation icon"""
return get_navigation_icon(view, size)
@staticmethod
def create_colored_icon(base_icon: str, color: str, size: int = 24) -> QIcon:
"""Create a colored version of an icon"""
return get_icon(base_icon, color, size)
@staticmethod
def traffic_light_icon(red_on: bool = False, yellow_on: bool = False, green_on: bool = False, size: int = 32) -> QIcon:
"""Create a traffic light icon with specific lights on/off"""
return create_traffic_light_icon(red_on, yellow_on, green_on, size)

View File

@@ -0,0 +1,51 @@
from PySide6.QtWidgets import QApplication
import sys
import os
import time
def main():
# Create application instance first
app = QApplication.instance() or QApplication(sys.argv)
# Show splash screen if available
splash = None
try:
from splash import show_splash
splash, app = show_splash(app)
except Exception as e:
print(f"Could not show splash screen: {e}")
# Add a short delay to show the splash screen
if splash:
time.sleep(1)
try:
# Try to use enhanced version with traffic light detection
from ..ui.main_window import MainWindow
print("✅ Using standard MainWindow")
except Exception as e:
# Fall back to standard version
print(f"⚠️ Could not load MainWindow: {e}")
sys.exit(1)
try:
# Initialize main window
window = MainWindow()
# Close splash if it exists
if splash:
splash.finish(window)
# Show main window
window.show()
# Start application event loop
sys.exit(app.exec())
except Exception as e:
print(f"❌ Error starting application: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,558 @@
"""
Finale UI - Modern Main Window
Advanced traffic monitoring interface with Material Design and dark theme.
Connects to existing detection/violation logic from qt_app_pyside.
"""
from PySide6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget,
QDockWidget, QSplitter, QFrame, QMessageBox, QApplication,
QFileDialog, QStatusBar, QMenuBar, QMenu, QToolBar
)
from PySide6.QtCore import Qt, QTimer, QSettings, QSize, Signal, Slot, QPropertyAnimation, QEasingCurve
from PySide6.QtGui import QIcon, QPixmap, QAction, QPainter, QBrush, QColor
import os
import sys
import json
import time
import traceback
from pathlib import Path
# Import finale UI components
try:
# Try relative imports first (when running as a package)
from .styles import FinaleStyles, MaterialColors
from .icons import FinaleIcons
from .toolbar import FinaleToolbar
from .components.stats_widgets import StatsWidget, MetricsWidget, SystemResourceWidget
from .views import LiveView, AnalyticsView, ViolationsView, SettingsView
except ImportError:
# Fallback to direct imports (when running as script)
try:
from styles import FinaleStyles, MaterialColors
from icons import FinaleIcons
from toolbar import FinaleToolbar
from components.stats_widgets import StatsWidget, MetricsWidget, SystemResourceWidget
from views import LiveView, AnalyticsView, ViolationsView, SettingsView
except ImportError:
print('Error importing main window components')
# Import existing detection/violation logic from qt_app_pyside
sys.path.append(str(Path(__file__).parent.parent))
try:
from controllers.model_manager import ModelManager
from controllers.video_controller_new import VideoController
from controllers.analytics_controller import AnalyticsController
from controllers.performance_overlay import PerformanceOverlay
# Import detection_openvino for advanced detection logic
from detection_openvino import OpenVINOVehicleDetector
from red_light_violation_pipeline import RedLightViolationPipeline
from utils.helpers import load_configuration, save_configuration
from utils.annotation_utils import draw_detections, convert_cv_to_pixmap
from utils.enhanced_annotation_utils import enhanced_draw_detections
from utils.traffic_light_utils import detect_traffic_light_color
except ImportError as e:
print(f"Warning: Could not import some dependencies: {e}")
# Fallback imports
from controllers.model_manager import ModelManager
VideoController = None
def load_configuration(path): return {}
def save_configuration(config, path): pass
class FinaleMainWindow(QMainWindow):
"""
Modern main window for traffic monitoring with advanced UI.
Connects to existing detection/violation logic without modifying it.
"""
# Signals for UI updates
theme_changed = Signal(bool) # dark_mode
view_changed = Signal(str) # view_name
fullscreen_toggled = Signal(bool)
def __init__(self):
super().__init__()
# Initialize settings and configuration
self.settings = QSettings("Finale", "TrafficMonitoring")
self.config_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "qt_app_pyside", "config.json")
self.config = load_configuration(self.config_file)
# UI state
self.dark_mode = True
self.current_view = "live"
self.is_fullscreen = False
# Animation system
self.animations = {}
# Initialize UI
self.setup_ui()
# Initialize backend controllers (existing logic)
self.setup_controllers()
# Connect signals
self.connect_signals()
# Apply theme and restore settings
self.apply_theme()
self.restore_settings()
# Show ready message
self.statusBar().showMessage("Finale UI Ready", 3000)
def setup_ui(self):
"""Set up the modern user interface"""
# Window properties with advanced styling
self.setWindowTitle("Finale Traffic Monitoring System")
self.setMinimumSize(1400, 900)
self.resize(1600, 1000)
# Set window icon
self.setWindowIcon(FinaleIcons.get_icon("traffic_monitoring"))
# Create central widget with modern layout
self.setup_central_widget()
# Create modern toolbar
self.setup_toolbar()
# Create docked widgets
self.setup_dock_widgets()
# Create status bar
self.setup_status_bar()
# Create menu bar
self.setup_menu_bar()
# Apply initial styling
self.setStyleSheet(FinaleStyles.get_main_window_style())
def setup_central_widget(self):
"""Create the central widget with modern tabbed interface"""
# Create main splitter for flexible layout
self.main_splitter = QSplitter(Qt.Horizontal)
# Create left panel for main content
self.content_widget = QWidget()
self.content_layout = QVBoxLayout(self.content_widget)
self.content_layout.setContentsMargins(0, 0, 0, 0)
self.content_layout.setSpacing(0)
# Create modern tab widget
self.tabs = QTabWidget()
self.tabs.setTabPosition(QTabWidget.North)
self.tabs.setMovable(True)
self.tabs.setTabsClosable(False)
# Create views (these will be implemented next)
self.live_view = LiveView()
self.analytics_view = AnalyticsView()
self.violations_view = ViolationsView()
self.settings_view = SettingsView()
# Add tabs with icons
self.tabs.addTab(self.live_view, FinaleIcons.get_icon("live"), "Live Detection")
self.tabs.addTab(self.analytics_view, FinaleIcons.get_icon("analytics"), "Analytics")
self.tabs.addTab(self.violations_view, FinaleIcons.get_icon("warning"), "Violations")
self.tabs.addTab(self.settings_view, FinaleIcons.get_icon("settings"), "Settings")
# Style the tab widget
self.tabs.setStyleSheet(FinaleStyles.get_tab_widget_style())
# Add to layout
self.content_layout.addWidget(self.tabs)
self.main_splitter.addWidget(self.content_widget)
# Set as central widget
self.setCentralWidget(self.main_splitter)
def setup_toolbar(self):
"""Create the modern toolbar"""
self.toolbar = FinaleToolbar(self)
self.addToolBar(Qt.TopToolBarArea, self.toolbar)
# Connect toolbar signals
self.toolbar.play_clicked.connect(self.on_play_clicked)
self.toolbar.pause_clicked.connect(self.on_pause_clicked)
self.toolbar.stop_clicked.connect(self.on_stop_clicked)
self.toolbar.record_clicked.connect(self.on_record_clicked)
self.toolbar.snapshot_clicked.connect(self.on_snapshot_clicked)
self.toolbar.settings_clicked.connect(self.show_settings)
self.toolbar.fullscreen_clicked.connect(self.toggle_fullscreen)
self.toolbar.theme_changed.connect(self.set_dark_mode)
def setup_dock_widgets(self):
"""Create docked widgets for statistics and controls"""
# Stats dock widget
self.stats_dock = QDockWidget("Statistics", self)
self.stats_dock.setObjectName("StatsDock")
self.stats_widget = StatsWidget()
self.stats_dock.setWidget(self.stats_widget)
self.stats_dock.setFeatures(
QDockWidget.DockWidgetMovable |
QDockWidget.DockWidgetClosable |
QDockWidget.DockWidgetFloatable
)
self.addDockWidget(Qt.RightDockWidgetArea, self.stats_dock)
# Metrics dock widget
self.metrics_dock = QDockWidget("Performance", self)
self.metrics_dock.setObjectName("MetricsDock")
self.metrics_widget = MetricsWidget()
self.metrics_dock.setWidget(self.metrics_widget)
self.metrics_dock.setFeatures(
QDockWidget.DockWidgetMovable |
QDockWidget.DockWidgetClosable |
QDockWidget.DockWidgetFloatable
)
self.addDockWidget(Qt.RightDockWidgetArea, self.metrics_dock)
# System resources dock widget
self.system_dock = QDockWidget("System", self)
self.system_dock.setObjectName("SystemDock")
self.system_widget = SystemResourceWidget()
self.system_dock.setWidget(self.system_widget)
self.system_dock.setFeatures(
QDockWidget.DockWidgetMovable |
QDockWidget.DockWidgetClosable |
QDockWidget.DockWidgetFloatable
)
self.addDockWidget(Qt.RightDockWidgetArea, self.system_dock)
# Tabify dock widgets for space efficiency
self.tabifyDockWidget(self.stats_dock, self.metrics_dock)
self.tabifyDockWidget(self.metrics_dock, self.system_dock)
# Show stats dock by default
self.stats_dock.raise_()
# Apply dock widget styling
for dock in [self.stats_dock, self.metrics_dock, self.system_dock]:
dock.setStyleSheet(FinaleStyles.get_dock_widget_style())
def setup_status_bar(self):
"""Create modern status bar"""
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
# Add permanent widgets to status bar
self.fps_label = QWidget()
self.connection_label = QWidget()
self.model_label = QWidget()
self.status_bar.addPermanentWidget(self.fps_label)
self.status_bar.addPermanentWidget(self.connection_label)
self.status_bar.addPermanentWidget(self.model_label)
# Style status bar
self.status_bar.setStyleSheet(FinaleStyles.get_status_bar_style())
def setup_menu_bar(self):
"""Create modern menu bar"""
self.menu_bar = self.menuBar()
# File menu
file_menu = self.menu_bar.addMenu("&File")
open_action = QAction(FinaleIcons.get_icon("folder"), "&Open Video", self)
open_action.setShortcut("Ctrl+O")
open_action.triggered.connect(self.open_file)
file_menu.addAction(open_action)
save_action = QAction(FinaleIcons.get_icon("save"), "&Save Config", self)
save_action.setShortcut("Ctrl+S")
save_action.triggered.connect(self.save_config)
file_menu.addAction(save_action)
file_menu.addSeparator()
exit_action = QAction(FinaleIcons.get_icon("exit"), "E&xit", self)
exit_action.setShortcut("Ctrl+Q")
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# View menu
view_menu = self.menu_bar.addMenu("&View")
fullscreen_action = QAction(FinaleIcons.get_icon("fullscreen"), "&Fullscreen", self)
fullscreen_action.setShortcut("F11")
fullscreen_action.setCheckable(True)
fullscreen_action.triggered.connect(self.toggle_fullscreen)
view_menu.addAction(fullscreen_action)
theme_action = QAction(FinaleIcons.get_icon("theme"), "&Dark Theme", self)
theme_action.setCheckable(True)
theme_action.setChecked(self.dark_mode)
theme_action.triggered.connect(self.toggle_theme)
view_menu.addAction(theme_action)
# Tools menu
tools_menu = self.menu_bar.addMenu("&Tools")
settings_action = QAction(FinaleIcons.get_icon("settings"), "&Settings", self)
settings_action.setShortcut("Ctrl+,")
settings_action.triggered.connect(self.show_settings)
tools_menu.addAction(settings_action)
# Help menu
help_menu = self.menu_bar.addMenu("&Help")
about_action = QAction(FinaleIcons.get_icon("info"), "&About", self)
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
# Style menu bar
self.menu_bar.setStyleSheet(FinaleStyles.get_menu_bar_style())
def setup_controllers(self):
"""Initialize backend controllers (existing logic)"""
try:
# Initialize model manager (existing from qt_app_pyside)
self.model_manager = ModelManager(self.config_file)
# Initialize video controller (existing from qt_app_pyside)
self.video_controller = VideoController(self.model_manager)
# Initialize analytics controller (existing from qt_app_pyside)
self.analytics_controller = AnalyticsController()
# Initialize performance overlay (existing from qt_app_pyside)
self.performance_overlay = PerformanceOverlay()
print("✅ Backend controllers initialized successfully")
except Exception as e:
print(f"❌ Error initializing controllers: {e}")
QMessageBox.critical(self, "Initialization Error",
f"Failed to initialize backend controllers:\n{str(e)}")
def connect_signals(self):
"""Connect signals between UI and backend"""
try:
# Connect video controller signals to UI updates
if hasattr(self.video_controller, 'frame_ready'):
self.video_controller.frame_ready.connect(self.on_frame_ready)
if hasattr(self.video_controller, 'stats_ready'):
self.video_controller.stats_ready.connect(self.on_stats_ready)
if hasattr(self.video_controller, 'violation_detected'):
self.video_controller.violation_detected.connect(self.on_violation_detected)
# Connect tab change signal
self.tabs.currentChanged.connect(self.on_tab_changed)
# Connect view signals to backend
self.live_view.source_changed.connect(self.on_source_changed)
print("✅ Signals connected successfully")
except Exception as e:
print(f"❌ Error connecting signals: {e}")
# Event handlers for UI interactions
@Slot()
def on_play_clicked(self):
"""Handle play button click"""
if hasattr(self.video_controller, 'start'):
self.video_controller.start()
self.toolbar.set_playback_state("playing")
@Slot()
def on_pause_clicked(self):
"""Handle pause button click"""
if hasattr(self.video_controller, 'pause'):
self.video_controller.pause()
self.toolbar.set_playback_state("paused")
@Slot()
def on_stop_clicked(self):
"""Handle stop button click"""
if hasattr(self.video_controller, 'stop'):
self.video_controller.stop()
self.toolbar.set_playback_state("stopped")
@Slot()
def on_record_clicked(self):
"""Handle record button click"""
# Implementation depends on existing recording logic
pass
@Slot()
def on_snapshot_clicked(self):
"""Handle snapshot button click"""
# Implementation depends on existing snapshot logic
pass
# Backend signal handlers
@Slot(object, object, dict)
def on_frame_ready(self, pixmap, detections, metrics):
"""Handle frame ready signal from video controller"""
# Update live view
if self.current_view == "live":
self.live_view.update_frame(pixmap, detections)
# Update toolbar status
self.toolbar.update_status("processing", True)
@Slot(dict)
def on_stats_ready(self, stats):
"""Handle stats ready signal from video controller"""
# Update stats widgets
self.stats_widget.update_stats(stats)
self.metrics_widget.update_metrics(stats)
# Update toolbar FPS
if 'fps' in stats:
self.toolbar.update_fps(stats['fps'])
@Slot(dict)
def on_violation_detected(self, violation_data):
"""Handle violation detected signal"""
# Update violations view
self.violations_view.add_violation(violation_data)
# Update toolbar status
self.toolbar.update_status("violation", True)
# Play notification sound/animation if enabled
self.play_violation_notification()
@Slot(str)
def on_source_changed(self, source_path):
"""Handle source change from live view"""
if hasattr(self.video_controller, 'set_source'):
self.video_controller.set_source(source_path)
@Slot(int)
def on_tab_changed(self, index):
"""Handle tab change"""
tab_names = ["live", "analytics", "violations", "settings"]
if 0 <= index < len(tab_names):
self.current_view = tab_names[index]
self.view_changed.emit(self.current_view)
# UI control methods
def toggle_fullscreen(self):
"""Toggle fullscreen mode"""
if self.isFullScreen():
self.showNormal()
self.is_fullscreen = False
else:
self.showFullScreen()
self.is_fullscreen = True
self.fullscreen_toggled.emit(self.is_fullscreen)
def toggle_theme(self):
"""Toggle between dark and light theme"""
self.set_dark_mode(not self.dark_mode)
def set_dark_mode(self, dark_mode):
"""Set theme mode"""
self.dark_mode = dark_mode
self.apply_theme()
self.theme_changed.emit(self.dark_mode)
def apply_theme(self):
"""Apply current theme to all UI elements"""
# Apply main styles
self.setStyleSheet(FinaleStyles.get_main_window_style(self.dark_mode))
# Update all child widgets
for child in self.findChildren(QWidget):
if hasattr(child, 'apply_theme'):
child.apply_theme(self.dark_mode)
# Update color scheme
if self.dark_mode:
MaterialColors.apply_dark_theme()
else:
MaterialColors.apply_light_theme()
def show_settings(self):
"""Show settings view"""
self.tabs.setCurrentWidget(self.settings_view)
def show_about(self):
"""Show about dialog"""
QMessageBox.about(self, "About Finale UI",
"Finale Traffic Monitoring System\n"
"Modern UI for OpenVINO-based traffic detection\n"
"Built with PySide6 and Material Design")
def open_file(self):
"""Open file dialog for video source"""
file_path, _ = QFileDialog.getOpenFileName(
self, "Open Video File", "",
"Video Files (*.mp4 *.avi *.mov *.mkv);;All Files (*)"
)
if file_path:
self.on_source_changed(file_path)
def save_config(self):
"""Save current configuration"""
try:
save_configuration(self.config, self.config_file)
self.statusBar().showMessage("Configuration saved", 3000)
except Exception as e:
QMessageBox.warning(self, "Save Error", f"Failed to save configuration:\n{str(e)}")
def play_violation_notification(self):
"""Play violation notification (visual/audio)"""
# Create a brief red flash animation
self.create_violation_flash()
def create_violation_flash(self):
"""Create a red flash effect for violations"""
# Create a semi-transparent red overlay
overlay = QWidget(self)
overlay.setStyleSheet("background-color: rgba(244, 67, 54, 0.3);")
overlay.resize(self.size())
overlay.show()
# Animate the overlay
self.flash_animation = QPropertyAnimation(overlay, b"windowOpacity")
self.flash_animation.setDuration(500)
self.flash_animation.setStartValue(0.3)
self.flash_animation.setEndValue(0.0)
self.flash_animation.setEasingCurve(QEasingCurve.OutCubic)
self.flash_animation.finished.connect(overlay.deleteLater)
self.flash_animation.start()
# Settings persistence
def save_settings(self):
"""Save window settings"""
self.settings.setValue("geometry", self.saveGeometry())
self.settings.setValue("windowState", self.saveState())
self.settings.setValue("dark_mode", self.dark_mode)
self.settings.setValue("current_view", self.current_view)
def restore_settings(self):
"""Restore window settings"""
if self.settings.contains("geometry"):
self.restoreGeometry(self.settings.value("geometry"))
if self.settings.contains("windowState"):
self.restoreState(self.settings.value("windowState"))
if self.settings.contains("dark_mode"):
self.dark_mode = self.settings.value("dark_mode", True, bool)
if self.settings.contains("current_view"):
view_name = self.settings.value("current_view", "live")
view_index = {"live": 0, "analytics": 1, "violations": 2, "settings": 3}.get(view_name, 0)
self.tabs.setCurrentIndex(view_index)
def closeEvent(self, event):
"""Handle window close event"""
# Save settings
self.save_settings()
# Stop video controller
if hasattr(self.video_controller, 'stop'):
self.video_controller.stop()
# Accept close event
event.accept()

View File

@@ -0,0 +1,641 @@
from PySide6.QtWidgets import (
QMainWindow, QTabWidget, QDockWidget, QMessageBox,
QApplication, QFileDialog, QSplashScreen
)
from PySide6.QtCore import Qt, QTimer, QSettings, QSize, Slot
from PySide6.QtGui import QIcon, QPixmap, QAction
import os
import sys
import json
import time
import traceback
from pathlib import Path
# Custom exception handler for Qt
def qt_message_handler(mode, context, message):
print(f"Qt Message: {message} (Mode: {mode})")
# Install custom handler for Qt messages
if hasattr(Qt, 'qInstallMessageHandler'):
Qt.qInstallMessageHandler(qt_message_handler)
# Import UI components
from ..ui.fixed_live_tab import LiveTab # Using fixed version
from ..ui.analytics_tab import AnalyticsTab
from ..ui.violations_tab import ViolationsTab
from ..ui.export_tab import ExportTab
from ..ui.config_panel import ConfigPanel
# Import controllers
from ..controllers.video_controller_new import VideoController
from ..controllers.analytics_controller import AnalyticsController
from ..controllers.performance_overlay import PerformanceOverlay
from ..controllers.model_manager import ModelManager
# Import utilities
from ..utils.helpers import load_configuration, save_configuration, save_snapshot
class MainWindow(QMainWindow):
"""Main application window."""
def __init__(self):
super().__init__()
# Initialize settings and configuration
self.settings = QSettings("OpenVINO", "TrafficMonitoring")
self.config_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.json")
self.config = load_configuration(self.config_file)
# Set up UI
self.setupUI()
# Initialize controllers
self.setupControllers()
# Connect signals and slots
self.connectSignals()
# Restore settings
self.restoreSettings()
# Apply theme
self.applyTheme(True) # Start with dark theme
# Show ready message
self.statusBar().showMessage("Ready")
def setupUI(self):
"""Set up the user interface"""
# Window properties
self.setWindowTitle("Traffic Monitoring System (OpenVINO PySide6)")
self.setMinimumSize(1200, 800)
self.resize(1400, 900)
# Set up central widget with tabs
self.tabs = QTabWidget()
# Create tabs
self.live_tab = LiveTab()
self.analytics_tab = AnalyticsTab()
self.violations_tab = ViolationsTab()
self.export_tab = ExportTab()
# Add tabs to tab widget
self.tabs.addTab(self.live_tab, "Live Detection")
self.tabs.addTab(self.analytics_tab, "Analytics")
self.tabs.addTab(self.violations_tab, "Violations")
self.tabs.addTab(self.export_tab, "Export & Config")
# Set central widget
self.setCentralWidget(self.tabs)
# Create config panel in dock widget
self.config_panel = ConfigPanel()
dock = QDockWidget("Settings", self)
dock.setObjectName("SettingsDock") # Set object name to avoid warning
dock.setWidget(self.config_panel)
dock.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetClosable)
dock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.addDockWidget(Qt.RightDockWidgetArea, dock)
# Create status bar
self.statusBar().showMessage("Initializing...")
# Create menu bar
self.setupMenus()
# Create performance overlay
self.performance_overlay = PerformanceOverlay()
def setupControllers(self):
"""Set up controllers and models"""
# Load config from file
try:
# Initialize model manager
self.model_manager = ModelManager(self.config_file)
# Create video controller
self.video_controller = VideoController(self.model_manager)
# Create analytics controller
self.analytics_controller = AnalyticsController()
# Setup update timer for performance overlay
self.perf_timer = QTimer()
self.perf_timer.timeout.connect(self.performance_overlay.update_stats)
self.perf_timer.start(1000) # Update every second
except Exception as e:
QMessageBox.critical(
self,
"Initialization Error",
f"Error initializing controllers: {str(e)}"
)
print(f"Error details: {e}")
def connectSignals(self):
"""Connect signals and slots between components""" # Video controller connections - With extra debug
print("🔌 Connecting video controller signals...")
try:
# Connect for UI frame updates (QPixmap-based)
self.video_controller.frame_ready.connect(self.live_tab.update_display, Qt.QueuedConnection)
print("✅ Connected frame_ready signal") # Connect for direct NumPy frame display (critical for live video)
try:
self.video_controller.frame_np_ready.connect(self.live_tab.update_display_np, Qt.QueuedConnection)
print("✅ Connected frame_np_ready signal")
# PySide6 doesn't have isConnected method, so let's just confirm the connection works
print("🔌 frame_np_ready connection should be established")
except Exception as e:
print(f"❌ Error connecting frame_np_ready signal: {e}")
import traceback
traceback.print_exc()
# Connect stats signal
self.video_controller.stats_ready.connect(self.live_tab.update_stats, Qt.QueuedConnection)
# Also connect stats signal to update traffic light status in main window
self.video_controller.stats_ready.connect(self.update_traffic_light_status, Qt.QueuedConnection)
print("✅ Connected stats_ready signals")
# Connect raw frame data for analytics
self.video_controller.raw_frame_ready.connect(self.analytics_controller.process_frame_data)
print("✅ Connected raw_frame_ready signal")
# Connect for traffic light status updates
self.video_controller.stats_ready.connect(self.update_traffic_light_status, Qt.QueuedConnection)
print("✅ Connected stats_ready signal to update_traffic_light_status")
# Connect violation detection signal
try:
self.video_controller.violation_detected.connect(self.handle_violation_detected, Qt.QueuedConnection)
print("✅ Connected violation_detected signal")
except Exception as e:
print(f"⚠️ Could not connect violation signal: {e}")
except Exception as e:
print(f"❌ Error connecting signals: {e}")
import traceback
traceback.print_exc()
# Live tab connections
self.live_tab.source_changed.connect(self.video_controller.set_source)
self.live_tab.video_dropped.connect(self.video_controller.set_source)
self.live_tab.snapshot_requested.connect(self.take_snapshot)
self.live_tab.run_requested.connect(self.toggle_video_processing)
# Config panel connections
self.config_panel.config_changed.connect(self.apply_config)
self.config_panel.theme_toggled.connect(self.applyTheme)
# Analytics controller connections
self.analytics_controller.analytics_updated.connect(self.analytics_tab.update_analytics)
self.analytics_controller.analytics_updated.connect(self.export_tab.update_export_preview)
# Tab-specific connections
self.violations_tab.clear_btn.clicked.connect(self.analytics_controller.clear_statistics)
self.export_tab.reset_btn.clicked.connect(self.config_panel.reset_config)
self.export_tab.save_config_btn.clicked.connect(self.save_config)
self.export_tab.reload_config_btn.clicked.connect(self.load_config)
self.export_tab.export_btn.clicked.connect(self.export_data)
def setupMenus(self):
"""Set up application menus"""
# File menu
file_menu = self.menuBar().addMenu("&File")
open_action = QAction("&Open Video...", self)
open_action.setShortcut("Ctrl+O")
open_action.triggered.connect(self.open_video_file)
file_menu.addAction(open_action)
file_menu.addSeparator()
snapshot_action = QAction("Take &Snapshot", self)
snapshot_action.setShortcut("Ctrl+S")
snapshot_action.triggered.connect(self.take_snapshot)
file_menu.addAction(snapshot_action)
file_menu.addSeparator()
exit_action = QAction("E&xit", self)
exit_action.setShortcut("Alt+F4")
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# View menu
view_menu = self.menuBar().addMenu("&View")
toggle_config_action = QAction("Show/Hide &Settings Panel", self)
toggle_config_action.setShortcut("F4")
toggle_config_action.triggered.connect(self.toggle_config_panel)
view_menu.addAction(toggle_config_action)
toggle_perf_action = QAction("Show/Hide &Performance Overlay", self)
toggle_perf_action.setShortcut("F5")
toggle_perf_action.triggered.connect(self.toggle_performance_overlay)
view_menu.addAction(toggle_perf_action)
# Help menu
help_menu = self.menuBar().addMenu("&Help")
about_action = QAction("&About", self)
about_action.triggered.connect(self.show_about_dialog)
help_menu.addAction(about_action)
@Slot(dict)
def apply_config(self, config):
"""
Apply configuration changes.
Args:
config: Configuration dictionary
"""
# Update configuration
if not config:
return
# Update config
for section in config:
if section in self.config:
self.config[section].update(config[section])
else:
self.config[section] = config[section]
# Update model manager
if self.model_manager:
self.model_manager.update_config(self.config)
# Save config to file
save_configuration(self.config, self.config_file)
# Update export tab
self.export_tab.update_config_display(self.config)
# Update status
self.statusBar().showMessage("Configuration applied", 2000)
@Slot()
def load_config(self):
"""Load configuration from file"""
# Ask for confirmation if needed
if self.video_controller and self.video_controller._running:
reply = QMessageBox.question(
self,
"Reload Configuration",
"Reloading configuration will stop current processing. Continue?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.No:
return
# Stop processing
self.video_controller.stop()
# Load config
self.config = load_configuration(self.config_file)
# Update UI
self.config_panel.set_config(self.config)
self.export_tab.update_config_display(self.config)
# Update model manager
if self.model_manager:
self.model_manager.update_config(self.config)
# Update status
self.statusBar().showMessage("Configuration loaded", 2000)
@Slot()
def save_config(self):
"""Save configuration to file"""
# Get config from UI
ui_config = self.export_tab.get_config_from_ui()
# Update config
for section in ui_config:
if section in self.config:
self.config[section].update(ui_config[section])
else:
self.config[section] = ui_config[section]
# Save to file
if save_configuration(self.config, self.config_file):
self.statusBar().showMessage("Configuration saved", 2000)
else:
self.statusBar().showMessage("Error saving configuration", 2000)
# Update model manager
if self.model_manager:
self.model_manager.update_config(self.config)
@Slot()
def open_video_file(self):
"""Open video file dialog"""
file_path, _ = QFileDialog.getOpenFileName(
self,
"Open Video File",
"",
"Video Files (*.mp4 *.avi *.mov *.mkv *.webm);;All Files (*)"
)
if file_path:
# Update live tab
self.live_tab.source_changed.emit(file_path)
# Update status
self.statusBar().showMessage(f"Loaded video: {os.path.basename(file_path)}")
@Slot()
def take_snapshot(self):
"""Take snapshot of current frame"""
if self.video_controller:
# Get current frame
frame = self.video_controller.capture_snapshot()
if frame is not None:
# Save frame to file
save_dir = self.settings.value("snapshot_dir", ".")
file_path = os.path.join(save_dir, "snapshot_" +
str(int(time.time())) + ".jpg")
saved_path = save_snapshot(frame, file_path)
if saved_path:
self.statusBar().showMessage(f"Snapshot saved: {saved_path}", 3000)
else:
self.statusBar().showMessage("Error saving snapshot", 3000)
else:
self.statusBar().showMessage("No frame to capture", 3000)
@Slot()
def toggle_config_panel(self):
"""Toggle configuration panel visibility"""
dock_widgets = self.findChildren(QDockWidget)
for dock in dock_widgets:
dock.setVisible(not dock.isVisible())
@Slot()
def toggle_performance_overlay(self):
"""Toggle performance overlay visibility"""
if self.performance_overlay.isVisible():
self.performance_overlay.hide()
else:
# Position in the corner
self.performance_overlay.move(self.pos().x() + 10, self.pos().y() + 30)
self.performance_overlay.show()
@Slot(bool)
def applyTheme(self, dark_theme):
"""
Apply light or dark theme.
Args:
dark_theme: True for dark theme, False for light theme
"""
if dark_theme:
# Load dark theme stylesheet
theme_file = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
"resources", "themes", "dark.qss"
)
else:
# Load light theme stylesheet
theme_file = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
"resources", "themes", "light.qss"
)
# Apply theme if file exists
if os.path.exists(theme_file):
with open(theme_file, "r") as f:
self.setStyleSheet(f.read())
else:
# Fallback to built-in style
self.setStyleSheet("")
@Slot()
def export_data(self):
"""Export data to file"""
export_format = self.export_tab.export_format_combo.currentText()
export_data = self.export_tab.export_data_combo.currentText()
# Get file type filter based on format
if export_format == "CSV":
file_filter = "CSV Files (*.csv)"
default_ext = ".csv"
elif export_format == "JSON":
file_filter = "JSON Files (*.json)"
default_ext = ".json"
elif export_format == "Excel":
file_filter = "Excel Files (*.xlsx)"
default_ext = ".xlsx"
elif export_format == "PDF Report":
file_filter = "PDF Files (*.pdf)"
default_ext = ".pdf"
else:
file_filter = "All Files (*)"
default_ext = ".txt"
# Get save path
file_path, _ = QFileDialog.getSaveFileName(
self,
"Export Data",
f"traffic_data{default_ext}",
file_filter
)
if not file_path:
return
try:
# Get analytics data
analytics = self.analytics_controller.get_analytics()
# Export based on format
if export_format == "CSV":
from ..utils.helpers import create_export_csv
result = create_export_csv(analytics['detection_counts'], file_path)
elif export_format == "JSON":
from ..utils.helpers import create_export_json
result = create_export_json(analytics, file_path)
elif export_format == "Excel":
# Requires openpyxl
try:
import pandas as pd
df = pd.DataFrame({
'Class': list(analytics['detection_counts'].keys()),
'Count': list(analytics['detection_counts'].values())
})
df.to_excel(file_path, index=False)
result = True
except Exception as e:
print(f"Excel export error: {e}")
result = False
else:
# Not implemented
QMessageBox.information(
self,
"Not Implemented",
f"Export to {export_format} is not yet implemented."
)
return
if result:
self.statusBar().showMessage(f"Data exported to {file_path}", 3000)
else:
self.statusBar().showMessage("Error exporting data", 3000)
except Exception as e:
QMessageBox.critical(
self,
"Export Error",
f"Error exporting data: {str(e)}"
)
@Slot()
def show_about_dialog(self):
"""Show about dialog"""
QMessageBox.about(
self,
"About Traffic Monitoring System",
"<h3>Traffic Monitoring System</h3>"
"<p>Based on OpenVINO™ and PySide6</p>"
"<p>Version 1.0.0</p>"
"<p>© 2025 GSOC Project</p>"
)
@Slot(bool)
def toggle_video_processing(self, start):
"""
Start or stop video processing.
Args:
start: True to start processing, False to stop
"""
if self.video_controller:
if start:
try:
# Make sure the source is correctly set to what the LiveTab has
current_source = self.live_tab.current_source
print(f"DEBUG: MainWindow toggle_processing with source: {current_source} (type: {type(current_source)})")
# Validate source
if current_source is None:
self.statusBar().showMessage("Error: No valid source selected")
return
# For file sources, verify file exists
if isinstance(current_source, str) and not current_source.isdigit():
if not os.path.exists(current_source):
self.statusBar().showMessage(f"Error: File not found: {current_source}")
return
# Ensure the source is set before starting
print(f"🎥 Setting video controller source to: {current_source}")
self.video_controller.set_source(current_source)
# Now start processing after a short delay to ensure source is set
print("⏱️ Scheduling video processing start after 200ms delay...")
QTimer.singleShot(200, lambda: self._start_video_processing())
source_desc = f"file: {os.path.basename(current_source)}" if isinstance(current_source, str) and os.path.exists(current_source) else f"camera: {current_source}"
self.statusBar().showMessage(f"Video processing started with {source_desc}")
except Exception as e:
print(f"❌ Error starting video: {e}")
traceback.print_exc()
self.statusBar().showMessage(f"Error: {str(e)}")
else:
try:
print("🛑 Stopping video processing...")
self.video_controller.stop()
print("✅ Video controller stopped")
self.statusBar().showMessage("Video processing stopped")
except Exception as e:
print(f"❌ Error stopping video: {e}")
traceback.print_exc()
def _start_video_processing(self):
"""Actual video processing start with extra error handling"""
try:
print("🚀 Starting video controller...")
self.video_controller.start()
print("✅ Video controller started successfully")
except Exception as e:
print(f"❌ Error in video processing start: {e}")
traceback.print_exc()
self.statusBar().showMessage(f"Video processing error: {str(e)}")
def closeEvent(self, event):
"""Handle window close event"""
# Stop processing
if self.video_controller and self.video_controller._running:
self.video_controller.stop()
# Save settings
self.saveSettings()
# Accept close event
event.accept()
def restoreSettings(self):
"""Restore application settings"""
# Restore window geometry
geometry = self.settings.value("geometry")
if geometry:
self.restoreGeometry(geometry)
# Restore window state
state = self.settings.value("windowState")
if state:
self.restoreState(state)
def saveSettings(self):
"""Save application settings"""
# Save window geometry
self.settings.setValue("geometry", self.saveGeometry())
# Save window state
self.settings.setValue("windowState", self.saveState())
# Save current directory as snapshot directory
self.settings.setValue("snapshot_dir", os.getcwd())
@Slot(dict)
def update_traffic_light_status(self, stats):
"""Update status bar with traffic light information if detected"""
traffic_light_info = stats.get('traffic_light_color', 'unknown')
# Handle both string and dictionary return formats
if isinstance(traffic_light_info, dict):
traffic_light_color = traffic_light_info.get('color', 'unknown')
confidence = traffic_light_info.get('confidence', 0.0)
confidence_str = f" (Confidence: {confidence:.2f})" if confidence > 0 else ""
else:
traffic_light_color = traffic_light_info
confidence_str = ""
if traffic_light_color != 'unknown':
current_message = self.statusBar().currentMessage()
if not current_message or "Traffic Light" not in current_message:
# Handle both dictionary and string formats
if isinstance(traffic_light_color, dict):
color_text = traffic_light_color.get("color", "unknown").upper()
else:
color_text = str(traffic_light_color).upper()
self.statusBar().showMessage(f"Traffic Light: {color_text}{confidence_str}")
@Slot(dict)
def handle_violation_detected(self, violation):
"""Handle a detected traffic violation"""
try:
# Flash red status message
self.statusBar().showMessage(f"🚨 RED LIGHT VIOLATION DETECTED - Vehicle ID: {violation['track_id']}", 5000)
# Add to violations tab
self.violations_tab.add_violation(violation)
# Update analytics
if self.analytics_controller:
self.analytics_controller.register_violation(violation)
print(f"🚨 Violation processed: {violation['id']} at {violation['timestamp']}")
except Exception as e:
print(f"❌ Error handling violation: {e}")
import traceback
traceback.print_exc()

View File

@@ -0,0 +1,41 @@
from PySide6.QtWidgets import QApplication, QSplashScreen
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QPixmap
import sys
import os
def show_splash(existing_app=None):
# Use existing app if provided, otherwise create a new one
app = existing_app or QApplication(sys.argv)
# Get the directory of the executable or script
if getattr(sys, 'frozen', False):
# Running as compiled executable
app_dir = os.path.dirname(sys.executable)
else:
# Running as script
app_dir = os.path.dirname(os.path.abspath(__file__))
# Look for splash image
splash_image = os.path.join(app_dir, 'resources', 'splash.png')
if not os.path.exists(splash_image):
splash_image = os.path.join(app_dir, 'splash.png')
if not os.path.exists(splash_image):
return None
# Create splash screen
pixmap = QPixmap(splash_image)
splash = QSplashScreen(pixmap, Qt.WindowStaysOnTopHint)
splash.show()
app.processEvents()
return splash, app
if __name__ == "__main__":
# This is for testing the splash screen independently
splash, app = show_splash()
# Close the splash after 3 seconds
QTimer.singleShot(3000, splash.close)
sys.exit(app.exec())

View File

@@ -0,0 +1,677 @@
"""
Modern Dark Theme and Styling System
===================================
Complete styling system with Material Design 3.0 principles, dark theme,
animations, and responsive design for the Traffic Monitoring Application.
Features:
- Material Design 3.0 dark theme
- Animated transitions and hover effects
- Responsive typography and spacing
- Custom widget styling
- Accent color system
- Professional gradients and shadows
"""
from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve, QRect, QTimer
from PySide6.QtGui import QFont, QColor, QPalette, QLinearGradient, QBrush
from PySide6.QtWidgets import QApplication, QWidget
from typing import Dict, Optional
import json
class Colors:
"""Material Design 3.0 Color Palette - Dark Theme"""
# Primary colors
PRIMARY_BACKGROUND = "#121212"
SECONDARY_BACKGROUND = "#1E1E1E"
SURFACE = "#2C2C2C"
SURFACE_VARIANT = "#383838"
# Accent colors
ACCENT_CYAN = "#00BCD4"
ACCENT_GREEN = "#4CAF50"
ACCENT_RED = "#FF5722"
ACCENT_YELLOW = "#FFC107"
ACCENT_BLUE = "#2196F3"
ACCENT_PURPLE = "#9C27B0"
# Text colors
TEXT_PRIMARY = "#FFFFFF"
TEXT_SECONDARY = "#B0B0B0"
TEXT_DISABLED = "#757575"
# State colors
SUCCESS = "#4CAF50"
WARNING = "#FF9800"
ERROR = "#F44336"
INFO = "#2196F3"
# Border and divider
BORDER = "#424242"
DIVIDER = "#2C2C2C"
# Interactive states
HOVER = "#404040"
PRESSED = "#505050"
SELECTED = "#1976D2"
FOCUS = "#03DAC6"
class Fonts:
"""Typography system with hierarchy"""
@staticmethod
def get_font(size: int = 10, weight: str = "normal", family: str = "Segoe UI") -> QFont:
"""Get a font with specified parameters"""
font = QFont(family, size)
weight_map = {
"light": QFont.Weight.Light,
"normal": QFont.Weight.Normal,
"medium": QFont.Weight.Medium,
"semibold": QFont.Weight.DemiBold,
"bold": QFont.Weight.Bold
}
font.setWeight(weight_map.get(weight, QFont.Weight.Normal))
return font
@staticmethod
def heading_1() -> QFont:
return Fonts.get_font(24, "bold")
@staticmethod
def heading_2() -> QFont:
return Fonts.get_font(20, "semibold")
@staticmethod
def heading_3() -> QFont:
return Fonts.get_font(16, "semibold")
@staticmethod
def body_large() -> QFont:
return Fonts.get_font(14, "normal")
@staticmethod
def body_medium() -> QFont:
return Fonts.get_font(12, "normal")
@staticmethod
def body_small() -> QFont:
return Fonts.get_font(10, "normal")
@staticmethod
def caption() -> QFont:
return Fonts.get_font(9, "normal")
@staticmethod
def button() -> QFont:
return Fonts.get_font(12, "medium")
class Spacing:
"""Consistent spacing system"""
XS = 4
SM = 8
MD = 16
LG = 24
XL = 32
XXL = 48
class BorderRadius:
"""Border radius system"""
SM = 4
MD = 8
LG = 12
XL = 16
PILL = 9999
class ThemeManager:
"""Manages application theme and styling"""
def __init__(self, accent_color: str = Colors.ACCENT_CYAN):
self.accent_color = accent_color
self._setup_palette()
def _setup_palette(self):
"""Setup Qt application palette"""
palette = QPalette()
# Window colors
palette.setColor(QPalette.Window, QColor(Colors.PRIMARY_BACKGROUND))
palette.setColor(QPalette.WindowText, QColor(Colors.TEXT_PRIMARY))
# Base colors (input fields)
palette.setColor(QPalette.Base, QColor(Colors.SURFACE))
palette.setColor(QPalette.Text, QColor(Colors.TEXT_PRIMARY))
# Button colors
palette.setColor(QPalette.Button, QColor(Colors.SURFACE))
palette.setColor(QPalette.ButtonText, QColor(Colors.TEXT_PRIMARY))
# Highlight colors
palette.setColor(QPalette.Highlight, QColor(self.accent_color))
palette.setColor(QPalette.HighlightedText, QColor(Colors.TEXT_PRIMARY))
# Apply palette
if QApplication.instance():
QApplication.instance().setPalette(palette)
def set_accent_color(self, color: str):
"""Change the accent color"""
self.accent_color = color
self._setup_palette()
class StyleSheets:
"""Collection of Qt StyleSheets for various components"""
@staticmethod
def main_window() -> str:
return f"""
QMainWindow {{
background-color: {Colors.PRIMARY_BACKGROUND};
color: {Colors.TEXT_PRIMARY};
}}
QMainWindow::separator {{
background-color: {Colors.BORDER};
width: 1px;
height: 1px;
}}
"""
@staticmethod
def tab_widget() -> str:
return f"""
QTabWidget::pane {{
border: 1px solid {Colors.BORDER};
background-color: {Colors.SECONDARY_BACKGROUND};
border-radius: {BorderRadius.MD}px;
}}
QTabBar::tab {{
background-color: {Colors.SURFACE};
color: {Colors.TEXT_SECONDARY};
padding: {Spacing.SM}px {Spacing.MD}px;
margin-right: 2px;
border-top-left-radius: {BorderRadius.SM}px;
border-top-right-radius: {BorderRadius.SM}px;
font-weight: 500;
min-width: 100px;
}}
QTabBar::tab:selected {{
background-color: {Colors.ACCENT_CYAN};
color: {Colors.TEXT_PRIMARY};
}}
QTabBar::tab:hover:!selected {{
background-color: {Colors.HOVER};
color: {Colors.TEXT_PRIMARY};
}}
"""
@staticmethod
def button_primary() -> str:
return f"""
QPushButton {{
background-color: {Colors.ACCENT_CYAN};
color: {Colors.TEXT_PRIMARY};
border: none;
padding: {Spacing.SM}px {Spacing.MD}px;
border-radius: {BorderRadius.SM}px;
font-weight: 500;
min-height: 32px;
}}
QPushButton:hover {{
background-color: #00ACC1;
}}
QPushButton:pressed {{
background-color: #0097A7;
}}
QPushButton:disabled {{
background-color: {Colors.SURFACE};
color: {Colors.TEXT_DISABLED};
}}
"""
@staticmethod
def button_secondary() -> str:
return f"""
QPushButton {{
background-color: transparent;
color: {Colors.ACCENT_CYAN};
border: 2px solid {Colors.ACCENT_CYAN};
padding: {Spacing.SM}px {Spacing.MD}px;
border-radius: {BorderRadius.SM}px;
font-weight: 500;
min-height: 32px;
}}
QPushButton:hover {{
background-color: rgba(0, 188, 212, 0.1);
}}
QPushButton:pressed {{
background-color: rgba(0, 188, 212, 0.2);
}}
"""
@staticmethod
def card() -> str:
return f"""
QWidget {{
background-color: {Colors.SURFACE};
border: 1px solid {Colors.BORDER};
border-radius: {BorderRadius.MD}px;
padding: {Spacing.MD}px;
}}
"""
@staticmethod
def input_field() -> str:
return f"""
QLineEdit, QTextEdit, QSpinBox, QDoubleSpinBox, QComboBox {{
background-color: {Colors.SURFACE};
color: {Colors.TEXT_PRIMARY};
border: 2px solid {Colors.BORDER};
border-radius: {BorderRadius.SM}px;
padding: {Spacing.SM}px;
font-size: 12px;
}}
QLineEdit:focus, QTextEdit:focus, QSpinBox:focus,
QDoubleSpinBox:focus, QComboBox:focus {{
border-color: {Colors.ACCENT_CYAN};
}}
QLineEdit:hover, QTextEdit:hover, QSpinBox:hover,
QDoubleSpinBox:hover, QComboBox:hover {{
border-color: {Colors.HOVER};
}}
"""
@staticmethod
def table() -> str:
return f"""
QTableWidget {{
background-color: {Colors.SURFACE};
color: {Colors.TEXT_PRIMARY};
gridline-color: {Colors.BORDER};
border: 1px solid {Colors.BORDER};
border-radius: {BorderRadius.SM}px;
}}
QTableWidget::item {{
padding: {Spacing.SM}px;
border-bottom: 1px solid {Colors.BORDER};
}}
QTableWidget::item:selected {{
background-color: {Colors.SELECTED};
}}
QTableWidget::item:hover {{
background-color: {Colors.HOVER};
}}
QHeaderView::section {{
background-color: {Colors.SURFACE_VARIANT};
color: {Colors.TEXT_PRIMARY};
padding: {Spacing.SM}px;
border: none;
font-weight: 600;
}}
"""
@staticmethod
def scroll_bar() -> str:
return f"""
QScrollBar:vertical {{
background-color: {Colors.SURFACE};
width: 12px;
border-radius: 6px;
}}
QScrollBar::handle:vertical {{
background-color: {Colors.BORDER};
border-radius: 6px;
min-height: 20px;
}}
QScrollBar::handle:vertical:hover {{
background-color: {Colors.HOVER};
}}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
height: 0px;
}}
QScrollBar:horizontal {{
background-color: {Colors.SURFACE};
height: 12px;
border-radius: 6px;
}}
QScrollBar::handle:horizontal {{
background-color: {Colors.BORDER};
border-radius: 6px;
min-width: 20px;
}}
QScrollBar::handle:horizontal:hover {{
background-color: {Colors.HOVER};
}}
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{
width: 0px;
}}
"""
@staticmethod
def progress_bar() -> str:
return f"""
QProgressBar {{
background-color: {Colors.SURFACE};
border: none;
border-radius: {BorderRadius.SM}px;
text-align: center;
height: 8px;
}}
QProgressBar::chunk {{
background-color: {Colors.ACCENT_CYAN};
border-radius: {BorderRadius.SM}px;
}}
"""
@staticmethod
def status_bar() -> str:
return f"""
QStatusBar {{
background-color: {Colors.SURFACE_VARIANT};
color: {Colors.TEXT_SECONDARY};
border-top: 1px solid {Colors.BORDER};
padding: {Spacing.SM}px;
}}
"""
@staticmethod
def toolbar() -> str:
return f"""
QToolBar {{
background-color: {Colors.SURFACE_VARIANT};
border: none;
spacing: {Spacing.SM}px;
padding: {Spacing.SM}px;
}}
QToolButton {{
background-color: transparent;
color: {Colors.TEXT_PRIMARY};
border: none;
border-radius: {BorderRadius.SM}px;
padding: {Spacing.SM}px;
min-width: 36px;
min-height: 36px;
}}
QToolButton:hover {{
background-color: {Colors.HOVER};
}}
QToolButton:pressed {{
background-color: {Colors.PRESSED};
}}
QToolButton:checked {{
background-color: {Colors.ACCENT_CYAN};
}}
"""
@staticmethod
def dock_widget() -> str:
return f"""
QDockWidget {{
background-color: {Colors.SECONDARY_BACKGROUND};
color: {Colors.TEXT_PRIMARY};
titlebar-close-icon: none;
titlebar-normal-icon: none;
}}
QDockWidget::title {{
background-color: {Colors.SURFACE_VARIANT};
padding: {Spacing.SM}px;
font-weight: 600;
}}
"""
class AnimationManager:
"""Manages UI animations and transitions"""
@staticmethod
def create_fade_animation(widget: QWidget, duration: int = 300) -> QPropertyAnimation:
"""Create a fade in/out animation"""
animation = QPropertyAnimation(widget, b"windowOpacity")
animation.setDuration(duration)
animation.setEasingCurve(QEasingCurve.InOutQuad)
return animation
@staticmethod
def create_slide_animation(widget: QWidget, start_pos: QRect, end_pos: QRect, duration: int = 300) -> QPropertyAnimation:
"""Create a slide animation"""
animation = QPropertyAnimation(widget, b"geometry")
animation.setDuration(duration)
animation.setStartValue(start_pos)
animation.setEndValue(end_pos)
animation.setEasingCurve(QEasingCurve.OutCubic)
return animation
@staticmethod
def pulse_widget(widget: QWidget, duration: int = 1000):
"""Create a pulsing effect on a widget"""
animation = QPropertyAnimation(widget, b"windowOpacity")
animation.setDuration(duration)
animation.setStartValue(1.0)
animation.setKeyValueAt(0.5, 0.5)
animation.setEndValue(1.0)
animation.setEasingCurve(QEasingCurve.InOutSine)
animation.setLoopCount(-1) # Infinite loop
animation.start()
return animation
def apply_theme(app: QApplication, theme_manager: Optional[ThemeManager] = None):
"""Apply the complete theme to the application"""
if not theme_manager:
theme_manager = ThemeManager()
# Set application style
app.setStyle("Fusion")
# Apply global stylesheet
global_style = f"""
* {{
font-family: "Segoe UI", "Inter", "Roboto", sans-serif;
}}
{StyleSheets.main_window()}
{StyleSheets.tab_widget()}
{StyleSheets.input_field()}
{StyleSheets.table()}
{StyleSheets.scroll_bar()}
{StyleSheets.progress_bar()}
{StyleSheets.status_bar()}
{StyleSheets.toolbar()}
{StyleSheets.dock_widget()}
QWidget {{
background-color: {Colors.PRIMARY_BACKGROUND};
color: {Colors.TEXT_PRIMARY};
}}
QGroupBox {{
background-color: {Colors.SURFACE};
border: 1px solid {Colors.BORDER};
border-radius: {BorderRadius.MD}px;
margin-top: {Spacing.MD}px;
padding-top: {Spacing.SM}px;
font-weight: 600;
}}
QGroupBox::title {{
subcontrol-origin: margin;
left: {Spacing.MD}px;
padding: 0 {Spacing.SM}px 0 {Spacing.SM}px;
}}
QCheckBox, QRadioButton {{
color: {Colors.TEXT_PRIMARY};
spacing: {Spacing.SM}px;
}}
QCheckBox::indicator, QRadioButton::indicator {{
width: 18px;
height: 18px;
border: 2px solid {Colors.BORDER};
border-radius: 4px;
background-color: {Colors.SURFACE};
}}
QCheckBox::indicator:checked, QRadioButton::indicator:checked {{
background-color: {Colors.ACCENT_CYAN};
border-color: {Colors.ACCENT_CYAN};
}}
QSlider::groove:horizontal {{
height: 6px;
background-color: {Colors.SURFACE};
border-radius: 3px;
}}
QSlider::handle:horizontal {{
background-color: {Colors.ACCENT_CYAN};
border: none;
width: 18px;
height: 18px;
border-radius: 9px;
margin: -6px 0;
}}
QSlider::sub-page:horizontal {{
background-color: {Colors.ACCENT_CYAN};
border-radius: 3px;
}}
QMenu {{
background-color: {Colors.SURFACE};
color: {Colors.TEXT_PRIMARY};
border: 1px solid {Colors.BORDER};
border-radius: {BorderRadius.SM}px;
padding: {Spacing.SM}px;
}}
QMenu::item {{
padding: {Spacing.SM}px {Spacing.MD}px;
border-radius: {BorderRadius.SM}px;
}}
QMenu::item:selected {{
background-color: {Colors.HOVER};
}}
QMenu::separator {{
height: 1px;
background-color: {Colors.BORDER};
margin: {Spacing.SM}px;
}}
QSplitter::handle {{
background-color: {Colors.BORDER};
}}
QSplitter::handle:horizontal {{
width: 2px;
}}
QSplitter::handle:vertical {{
height: 2px;
}}
"""
app.setStyleSheet(global_style)
# Utility functions for common styling patterns
def create_stat_card_style(accent_color: str = Colors.ACCENT_CYAN) -> str:
"""Create a styled card for statistics display"""
return f"""
QWidget {{
background-color: {Colors.SURFACE};
border: 1px solid {Colors.BORDER};
border-left: 4px solid {accent_color};
border-radius: {BorderRadius.MD}px;
padding: {Spacing.MD}px;
}}
QLabel {{
background-color: transparent;
border: none;
}}
"""
def create_alert_style(alert_type: str = "info") -> str:
"""Create styled alert components"""
color_map = {
"success": Colors.SUCCESS,
"warning": Colors.WARNING,
"error": Colors.ERROR,
"info": Colors.INFO
}
color = color_map.get(alert_type, Colors.INFO)
return f"""
QWidget {{
background-color: rgba({int(color[1:3], 16)}, {int(color[3:5], 16)}, {int(color[5:7], 16)}, 0.1);
border: 1px solid {color};
border-radius: {BorderRadius.SM}px;
padding: {Spacing.MD}px;
}}
QLabel {{
color: {color};
background-color: transparent;
border: none;
font-weight: 500;
}}
"""
class MaterialColors:
"""Alias for Colors for compatibility with old code."""
primary = Colors.ACCENT_CYAN
primary_variant = Colors.ACCENT_BLUE
secondary = Colors.ACCENT_GREEN
surface = Colors.SURFACE
text_primary = Colors.TEXT_PRIMARY
text_on_primary = Colors.TEXT_PRIMARY
class FinaleStyles:
"""Basic style helpers for compatibility with old code."""
@staticmethod
def get_group_box_style():
return """
QGroupBox {
border: 1px solid #424242;
border-radius: 8px;
margin-top: 8px;
background-color: #232323;
}
QGroupBox:title {
subcontrol-origin: margin;
left: 10px;
padding: 0 3px 0 3px;
color: #B0B0B0;
}
"""

View File

@@ -0,0 +1,476 @@
"""
Analytics View - Traffic analytics and reporting
Displays charts, statistics, and historical data.
"""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QGroupBox, QGridLayout, QFrame, QScrollArea, QTabWidget,
QTableWidget, QTableWidgetItem, QHeaderView, QDateEdit,
QComboBox, QSpinBox
)
from PySide6.QtCore import Qt, Signal, Slot, QTimer, QDate
from PySide6.QtGui import QPixmap, QPainter, QBrush, QColor, QFont
from datetime import datetime, timedelta
import json
# Import finale components
try:
# Try relative imports first (when running as a package)
from ..styles import FinaleStyles, MaterialColors
from ..icons import FinaleIcons
# Import advanced chart components from original analytics_tab
import sys
import os
from pathlib import Path
# Add parent directory to path to import from qt_app_pyside
sys.path.append(str(Path(__file__).parent.parent.parent))
from qt_app_pyside.ui.analytics_tab import ChartWidget, TimeSeriesChart, DetectionPieChart, ViolationBarChart
from qt_app_pyside.controllers.analytics_controller import AnalyticsController
from qt_app_pyside.utils.helpers import load_configuration, format_timestamp, format_duration
except ImportError:
# Fallback for direct execution
try:
from styles import FinaleStyles, MaterialColors
from icons import FinaleIcons
# Create simplified chart widgets if advanced ones not available
except ImportError:
print("Error importing analytics components")
class ChartWidget(QWidget):
def __init__(self, title="Chart"):
super().__init__()
self.title = title
self.data = []
self.chart_type = "line" # line, bar, pie
self.setMinimumSize(400, 300)
def set_data(self, data, chart_type="line"):
"""Set chart data and type"""
self.data = data
self.chart_type = chart_type
self.update()
def paintEvent(self, event):
"""Paint the chart"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# Background
painter.fillRect(self.rect(), QColor(MaterialColors.surface))
# Border
painter.setPen(QColor(MaterialColors.outline))
painter.drawRect(self.rect().adjusted(0, 0, -1, -1))
# Title
painter.setPen(QColor(MaterialColors.text_primary))
painter.setFont(QFont("Segoe UI", 12, QFont.Bold))
title_rect = self.rect().adjusted(10, 10, -10, -10)
painter.drawText(title_rect, Qt.AlignTop | Qt.AlignLeft, self.title)
# Chart area
chart_rect = self.rect().adjusted(50, 50, -20, -50)
if not self.data:
# No data message
painter.setPen(QColor(MaterialColors.text_secondary))
painter.setFont(QFont("Segoe UI", 10))
painter.drawText(chart_rect, Qt.AlignCenter, "No data available")
return
# Draw chart based on type
if self.chart_type == "line":
self.draw_line_chart(painter, chart_rect)
elif self.chart_type == "bar":
self.draw_bar_chart(painter, chart_rect)
elif self.chart_type == "pie":
self.draw_pie_chart(painter, chart_rect)
def draw_line_chart(self, painter, rect):
"""Draw a line chart"""
if len(self.data) < 2:
return
# Find min/max values
values = [item.get('value', 0) for item in self.data]
min_val, max_val = min(values), max(values)
if max_val == min_val:
max_val = min_val + 1
# Calculate points
points = []
for i, item in enumerate(self.data):
x = rect.left() + (i / (len(self.data) - 1)) * rect.width()
y = rect.bottom() - ((item.get('value', 0) - min_val) / (max_val - min_val)) * rect.height()
points.append((x, y))
# Draw grid lines
painter.setPen(QColor(MaterialColors.outline_variant))
for i in range(5):
y = rect.top() + (i / 4) * rect.height()
painter.drawLine(rect.left(), y, rect.right(), y)
# Draw line
painter.setPen(QColor(MaterialColors.primary))
for i in range(len(points) - 1):
painter.drawLine(points[i][0], points[i][1], points[i+1][0], points[i+1][1])
# Draw points
painter.setBrush(QBrush(QColor(MaterialColors.primary)))
for x, y in points:
painter.drawEllipse(x-3, y-3, 6, 6)
def draw_bar_chart(self, painter, rect):
"""Draw a bar chart"""
if not self.data:
return
values = [item.get('value', 0) for item in self.data]
max_val = max(values) if values else 1
bar_width = rect.width() / len(self.data) * 0.8
spacing = rect.width() / len(self.data) * 0.2
painter.setBrush(QBrush(QColor(MaterialColors.primary)))
for i, item in enumerate(self.data):
value = item.get('value', 0)
height = (value / max_val) * rect.height()
x = rect.left() + i * (bar_width + spacing) + spacing / 2
y = rect.bottom() - height
painter.drawRect(x, y, bar_width, height)
def draw_pie_chart(self, painter, rect):
"""Draw a pie chart"""
if not self.data:
return
total = sum(item.get('value', 0) for item in self.data)
if total == 0:
return
# Calculate center and radius
center = rect.center()
radius = min(rect.width(), rect.height()) // 2 - 20
# Colors for pie slices
colors = [MaterialColors.primary, MaterialColors.secondary, MaterialColors.tertiary,
MaterialColors.error, MaterialColors.success, MaterialColors.warning]
start_angle = 0
for i, item in enumerate(self.data):
value = item.get('value', 0)
angle = (value / total) * 360 * 16 # Qt uses 16ths of a degree
color = QColor(colors[i % len(colors)])
painter.setBrush(QBrush(color))
painter.setPen(QColor(MaterialColors.outline))
painter.drawPie(center.x() - radius, center.y() - radius,
radius * 2, radius * 2, start_angle, angle)
start_angle += angle
class TrafficSummaryWidget(QGroupBox):
"""
Widget showing traffic summary statistics.
"""
def __init__(self, parent=None):
super().__init__("Traffic Summary", parent)
self.setup_ui()
self.reset_stats()
def setup_ui(self):
"""Setup summary UI"""
layout = QGridLayout(self)
# Create stat labels
self.total_vehicles_label = QLabel("0")
self.total_violations_label = QLabel("0")
self.avg_speed_label = QLabel("0.0 km/h")
self.peak_hour_label = QLabel("N/A")
# Style the stat values
for label in [self.total_vehicles_label, self.total_violations_label,
self.avg_speed_label, self.peak_hour_label]:
label.setFont(QFont("Segoe UI", 16, QFont.Bold))
label.setStyleSheet(f"color: {MaterialColors.primary};")
# Add to layout
layout.addWidget(QLabel("Total Vehicles:"), 0, 0)
layout.addWidget(self.total_vehicles_label, 0, 1)
layout.addWidget(QLabel("Total Violations:"), 1, 0)
layout.addWidget(self.total_violations_label, 1, 1)
layout.addWidget(QLabel("Average Speed:"), 2, 0)
layout.addWidget(self.avg_speed_label, 2, 1)
layout.addWidget(QLabel("Peak Hour:"), 3, 0)
layout.addWidget(self.peak_hour_label, 3, 1)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
def reset_stats(self):
"""Reset all statistics"""
self.total_vehicles_label.setText("0")
self.total_violations_label.setText("0")
self.avg_speed_label.setText("0.0 km/h")
self.peak_hour_label.setText("N/A")
def update_stats(self, stats):
"""Update statistics display"""
if 'total_vehicles' in stats:
self.total_vehicles_label.setText(str(stats['total_vehicles']))
if 'total_violations' in stats:
self.total_violations_label.setText(str(stats['total_violations']))
if 'avg_speed' in stats:
self.avg_speed_label.setText(f"{stats['avg_speed']:.1f} km/h")
if 'peak_hour' in stats:
self.peak_hour_label.setText(stats['peak_hour'])
class ViolationsTableWidget(QTableWidget):
"""
Table widget for displaying violation records.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setup_table()
def setup_table(self):
"""Setup the violations table"""
# Set columns
columns = ["Time", "Type", "Vehicle", "Location", "Confidence", "Actions"]
self.setColumnCount(len(columns))
self.setHorizontalHeaderLabels(columns)
# Configure table
self.horizontalHeader().setStretchLastSection(True)
self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
self.setSelectionBehavior(QTableWidget.SelectRows)
self.setAlternatingRowColors(True)
# Apply styling
self.setStyleSheet(FinaleStyles.get_table_style())
def add_violation(self, violation_data):
"""Add a violation record to the table"""
row = self.rowCount()
self.insertRow(row)
# Populate row data
time_str = violation_data.get('timestamp', datetime.now().strftime('%H:%M:%S'))
violation_type = violation_data.get('type', 'Red Light')
vehicle_id = violation_data.get('vehicle_id', 'Unknown')
location = violation_data.get('location', 'Intersection 1')
confidence = violation_data.get('confidence', 0.0)
self.setItem(row, 0, QTableWidgetItem(time_str))
self.setItem(row, 1, QTableWidgetItem(violation_type))
self.setItem(row, 2, QTableWidgetItem(vehicle_id))
self.setItem(row, 3, QTableWidgetItem(location))
self.setItem(row, 4, QTableWidgetItem(f"{confidence:.2f}"))
# Actions button
actions_btn = QPushButton("View Details")
actions_btn.clicked.connect(lambda: self.view_violation_details(violation_data))
self.setCellWidget(row, 5, actions_btn)
# Auto-scroll to new violation
self.scrollToBottom()
def view_violation_details(self, violation_data):
"""View detailed violation information"""
# This could open a detailed dialog
print(f"Viewing violation details: {violation_data}")
class AnalyticsView(QWidget):
"""
Main analytics view with charts, statistics, and violation history.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.analytics_controller = AnalyticsController()
self.setup_ui()
self.analytics_controller.data_updated.connect(self.refresh_analytics)
# Load config if needed
self.config = load_configuration('config.json')
def setup_ui(self):
"""Setup the analytics view UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(16)
# Top controls
controls_layout = QHBoxLayout()
# Date range selection
controls_layout.addWidget(QLabel("Date Range:"))
self.start_date = QDateEdit()
self.start_date.setDate(QDate.currentDate().addDays(-7))
self.start_date.setCalendarPopup(True)
controls_layout.addWidget(self.start_date)
controls_layout.addWidget(QLabel("to"))
self.end_date = QDateEdit()
self.end_date.setDate(QDate.currentDate())
self.end_date.setCalendarPopup(True)
controls_layout.addWidget(self.end_date)
# Time interval
controls_layout.addWidget(QLabel("Interval:"))
self.interval_combo = QComboBox()
self.interval_combo.addItems(["Hourly", "Daily", "Weekly"])
controls_layout.addWidget(self.interval_combo)
# Refresh button
self.refresh_btn = QPushButton(FinaleIcons.get_icon("refresh"), "Refresh")
self.refresh_btn.clicked.connect(self.refresh_data)
controls_layout.addWidget(self.refresh_btn)
controls_layout.addStretch()
layout.addLayout(controls_layout)
# Main content area
content_layout = QHBoxLayout()
# Left panel - Charts
charts_widget = QWidget()
charts_layout = QVBoxLayout(charts_widget)
# Traffic flow chart
self.traffic_chart = AnalyticsChartWidget("Traffic Flow Over Time")
charts_layout.addWidget(self.traffic_chart)
# Violation types chart
self.violations_chart = AnalyticsChartWidget("Violation Types")
charts_layout.addWidget(self.violations_chart)
content_layout.addWidget(charts_widget, 2)
# Right panel - Statistics and table
right_panel = QVBoxLayout()
# Summary statistics
self.summary_widget = TrafficSummaryWidget()
right_panel.addWidget(self.summary_widget)
# Recent violations table
violations_group = QGroupBox("Recent Violations")
violations_layout = QVBoxLayout(violations_group)
self.violations_table = ViolationsTableWidget()
violations_layout.addWidget(self.violations_table)
violations_group.setStyleSheet(FinaleStyles.get_group_box_style())
right_panel.addWidget(violations_group, 1)
content_layout.addLayout(right_panel, 1)
layout.addLayout(content_layout, 1)
# Apply theme
self.apply_theme(True)
# Load initial data
self.refresh_data()
@Slot()
def refresh_data(self):
"""Refresh analytics data"""
print("Refreshing analytics data...")
# Update traffic flow chart (sample data)
traffic_data = [
{'label': '08:00', 'value': 45},
{'label': '09:00', 'value': 67},
{'label': '10:00', 'value': 89},
{'label': '11:00', 'value': 76},
{'label': '12:00', 'value': 92},
{'label': '13:00', 'value': 84},
{'label': '14:00', 'value': 71}
]
self.traffic_chart.set_data(traffic_data, "line")
# Update violations chart
violations_data = [
{'label': 'Red Light', 'value': 12},
{'label': 'Speed', 'value': 8},
{'label': 'Wrong Lane', 'value': 5},
{'label': 'No Helmet', 'value': 3}
]
self.violations_chart.set_data(violations_data, "pie")
# Update summary
summary_stats = {
'total_vehicles': 1247,
'total_violations': 28,
'avg_speed': 35.2,
'peak_hour': '12:00-13:00'
}
self.summary_widget.update_stats(summary_stats)
def refresh_analytics(self):
"""Refresh analytics data from controller"""
data = self.analytics_controller.get_analytics_data()
# Use format_timestamp, format_duration for display
# ... update charts and stats with new data ...
def update_demo_data(self):
"""Update with demo data for demonstration"""
import random
# Simulate new violation
if random.random() < 0.3: # 30% chance
violation = {
'timestamp': datetime.now().strftime('%H:%M:%S'),
'type': random.choice(['Red Light', 'Speed', 'Wrong Lane']),
'vehicle_id': f"VH{random.randint(1000, 9999)}",
'location': f"Intersection {random.randint(1, 5)}",
'confidence': random.uniform(0.7, 0.95)
}
self.violations_table.add_violation(violation)
def add_violation(self, violation_data):
"""Add a new violation (called from main window)"""
self.violations_table.add_violation(violation_data)
def apply_theme(self, dark_mode=True):
"""Apply theme to the view"""
if dark_mode:
self.setStyleSheet(f"""
QWidget {{
background-color: {MaterialColors.surface};
color: {MaterialColors.text_primary};
}}
QPushButton {{
background-color: {MaterialColors.primary};
color: {MaterialColors.text_on_primary};
border: none;
border-radius: 6px;
padding: 8px 16px;
}}
QPushButton:hover {{
background-color: {MaterialColors.primary_variant};
}}
QDateEdit, QComboBox {{
background-color: {MaterialColors.surface_variant};
border: 1px solid {MaterialColors.outline};
border-radius: 4px;
padding: 6px;
}}
""")

View File

@@ -0,0 +1,421 @@
"""
Live View - Real-time detection and monitoring
Connects to existing video controller and live detection logic.
"""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QFileDialog, QComboBox, QSlider, QSpinBox, QGroupBox,
QGridLayout, QFrame, QSizePolicy, QScrollArea
)
from PySide6.QtCore import Qt, Signal, Slot, QTimer, QSize
from PySide6.QtGui import QPixmap, QPainter, QBrush, QColor, QFont
import cv2
import numpy as np
from pathlib import Path
# Import finale components
from ..styles import FinaleStyles, MaterialColors
from ..icons import FinaleIcons
class VideoDisplayWidget(QLabel):
"""
Advanced video display widget with overlays and interactions.
"""
frame_clicked = Signal(int, int) # x, y coordinates
def __init__(self, parent=None):
super().__init__(parent)
self.setMinimumSize(640, 480)
self.setScaledContents(True)
self.setAlignment(Qt.AlignCenter)
self.setStyleSheet("""
QLabel {
border: 2px solid #424242;
border-radius: 8px;
background-color: #1a1a1a;
}
""")
# State
self.current_pixmap = None
self.overlay_enabled = True
# Default placeholder
self.set_placeholder()
def set_placeholder(self):
"""Set placeholder image when no video is loaded"""
placeholder = QPixmap(640, 480)
placeholder.fill(QColor(26, 26, 26))
painter = QPainter(placeholder)
painter.setPen(QColor(117, 117, 117))
painter.setFont(QFont("Segoe UI", 16))
painter.drawText(placeholder.rect(), Qt.AlignCenter, "No Video Source\nClick to select a file")
painter.end()
self.setPixmap(placeholder)
def update_frame(self, pixmap, detections=None):
"""Update frame with detections overlay"""
if pixmap is None:
return
self.current_pixmap = pixmap
if self.overlay_enabled and detections:
# Draw detection overlays
pixmap = self.add_detection_overlay(pixmap, detections)
self.setPixmap(pixmap)
def add_detection_overlay(self, pixmap, detections):
"""Add detection overlays to pixmap"""
if not detections:
return pixmap
# Create a copy to draw on
overlay_pixmap = QPixmap(pixmap)
painter = QPainter(overlay_pixmap)
# Draw detection boxes
for detection in detections:
# Extract detection info (format depends on backend)
if isinstance(detection, dict):
bbox = detection.get('bbox', [])
confidence = detection.get('confidence', 0.0)
class_name = detection.get('class', 'unknown')
else:
# Handle other detection formats
continue
if len(bbox) >= 4:
x1, y1, x2, y2 = bbox[:4]
# Draw bounding box
painter.setPen(QColor(MaterialColors.primary))
painter.drawRect(int(x1), int(y1), int(x2-x1), int(y2-y1))
# Draw label
label = f"{class_name}: {confidence:.2f}"
painter.setPen(QColor(MaterialColors.text_primary))
painter.drawText(int(x1), int(y1-5), label)
painter.end()
return overlay_pixmap
def mousePressEvent(self, event):
"""Handle mouse click events"""
if event.button() == Qt.LeftButton:
self.frame_clicked.emit(event.x(), event.y())
super().mousePressEvent(event)
class SourceControlWidget(QGroupBox):
"""
Widget for controlling video source (file, camera, stream).
"""
source_changed = Signal(str) # source path/url
def __init__(self, parent=None):
super().__init__("Video Source", parent)
self.setup_ui()
def setup_ui(self):
"""Setup the source control UI"""
layout = QVBoxLayout(self)
# Source type selection
source_layout = QHBoxLayout()
self.source_combo = QComboBox()
self.source_combo.addItems(["Select Source", "Video File", "Camera", "RTSP Stream"])
self.source_combo.currentTextChanged.connect(self.on_source_type_changed)
self.browse_btn = QPushButton(FinaleIcons.get_icon("folder"), "Browse")
self.browse_btn.clicked.connect(self.browse_file)
self.browse_btn.setEnabled(False)
source_layout.addWidget(QLabel("Type:"))
source_layout.addWidget(self.source_combo)
source_layout.addWidget(self.browse_btn)
layout.addLayout(source_layout)
# Source path/URL input
path_layout = QHBoxLayout()
self.path_label = QLabel("Path/URL:")
self.path_display = QLabel("No source selected")
self.path_display.setStyleSheet("QLabel { color: #757575; font-style: italic; }")
path_layout.addWidget(self.path_label)
path_layout.addWidget(self.path_display, 1)
layout.addLayout(path_layout)
# Camera settings (initially hidden)
self.camera_widget = QWidget()
camera_layout = QHBoxLayout(self.camera_widget)
camera_layout.addWidget(QLabel("Camera ID:"))
self.camera_spin = QSpinBox()
self.camera_spin.setRange(0, 10)
camera_layout.addWidget(self.camera_spin)
camera_layout.addStretch()
self.camera_widget.hide()
layout.addWidget(self.camera_widget)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
@Slot(str)
def on_source_type_changed(self, source_type):
"""Handle source type change"""
if source_type == "Video File":
self.browse_btn.setEnabled(True)
self.camera_widget.hide()
elif source_type == "Camera":
self.browse_btn.setEnabled(False)
self.camera_widget.show()
self.path_display.setText(f"Camera {self.camera_spin.value()}")
self.source_changed.emit(str(self.camera_spin.value()))
elif source_type == "RTSP Stream":
self.browse_btn.setEnabled(False)
self.camera_widget.hide()
# Could add RTSP URL input here
else:
self.browse_btn.setEnabled(False)
self.camera_widget.hide()
@Slot()
def browse_file(self):
"""Browse for video file"""
file_path, _ = QFileDialog.getOpenFileName(
self, "Select Video File", "",
"Video Files (*.mp4 *.avi *.mov *.mkv *.wmv);;All Files (*)"
)
if file_path:
self.path_display.setText(file_path)
self.source_changed.emit(file_path)
class DetectionControlWidget(QGroupBox):
"""
Widget for controlling detection parameters.
"""
confidence_changed = Signal(float)
nms_threshold_changed = Signal(float)
def __init__(self, parent=None):
super().__init__("Detection Settings", parent)
self.setup_ui()
def setup_ui(self):
"""Setup detection control UI"""
layout = QGridLayout(self)
# Confidence threshold
layout.addWidget(QLabel("Confidence:"), 0, 0)
self.confidence_slider = QSlider(Qt.Horizontal)
self.confidence_slider.setRange(1, 100)
self.confidence_slider.setValue(30)
self.confidence_slider.valueChanged.connect(self.on_confidence_changed)
self.confidence_label = QLabel("0.30")
self.confidence_label.setMinimumWidth(40)
layout.addWidget(self.confidence_slider, 0, 1)
layout.addWidget(self.confidence_label, 0, 2)
# NMS threshold
layout.addWidget(QLabel("NMS Threshold:"), 1, 0)
self.nms_slider = QSlider(Qt.Horizontal)
self.nms_slider.setRange(1, 100)
self.nms_slider.setValue(45)
self.nms_slider.valueChanged.connect(self.on_nms_changed)
self.nms_label = QLabel("0.45")
self.nms_label.setMinimumWidth(40)
layout.addWidget(self.nms_slider, 1, 1)
layout.addWidget(self.nms_label, 1, 2)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
@Slot(int)
def on_confidence_changed(self, value):
"""Handle confidence threshold change"""
confidence = value / 100.0
self.confidence_label.setText(f"{confidence:.2f}")
self.confidence_changed.emit(confidence)
@Slot(int)
def on_nms_changed(self, value):
"""Handle NMS threshold change"""
nms = value / 100.0
self.nms_label.setText(f"{nms:.2f}")
self.nms_threshold_changed.emit(nms)
class LiveView(QWidget):
"""
Main live detection view.
Displays real-time video with detection overlays and controls.
"""
source_changed = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.setup_ui()
self.current_detections = []
def setup_ui(self):
"""Setup the live view UI"""
layout = QHBoxLayout(self)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(16)
# Main video display area
video_layout = QVBoxLayout()
self.video_widget = VideoDisplayWidget()
self.video_widget.frame_clicked.connect(self.on_frame_clicked)
video_layout.addWidget(self.video_widget, 1)
# Video controls
controls_layout = QHBoxLayout()
self.play_btn = QPushButton(FinaleIcons.get_icon("play"), "")
self.play_btn.setToolTip("Play/Pause")
self.play_btn.setFixedSize(40, 40)
self.stop_btn = QPushButton(FinaleIcons.get_icon("stop"), "")
self.stop_btn.setToolTip("Stop")
self.stop_btn.setFixedSize(40, 40)
self.record_btn = QPushButton(FinaleIcons.get_icon("record"), "")
self.record_btn.setToolTip("Record")
self.record_btn.setFixedSize(40, 40)
self.record_btn.setCheckable(True)
self.snapshot_btn = QPushButton(FinaleIcons.get_icon("camera"), "")
self.snapshot_btn.setToolTip("Take Snapshot")
self.snapshot_btn.setFixedSize(40, 40)
controls_layout.addWidget(self.play_btn)
controls_layout.addWidget(self.stop_btn)
controls_layout.addWidget(self.record_btn)
controls_layout.addWidget(self.snapshot_btn)
controls_layout.addStretch()
# Overlay toggle
self.overlay_btn = QPushButton(FinaleIcons.get_icon("visibility"), "Overlays")
self.overlay_btn.setCheckable(True)
self.overlay_btn.setChecked(True)
self.overlay_btn.toggled.connect(self.toggle_overlays)
controls_layout.addWidget(self.overlay_btn)
video_layout.addLayout(controls_layout)
layout.addLayout(video_layout, 3)
# Right panel for controls
right_panel = QVBoxLayout()
# Source control
self.source_control = SourceControlWidget()
self.source_control.source_changed.connect(self.source_changed.emit)
right_panel.addWidget(self.source_control)
# Detection control
self.detection_control = DetectionControlWidget()
right_panel.addWidget(self.detection_control)
# Detection info
self.info_widget = QGroupBox("Detection Info")
info_layout = QVBoxLayout(self.info_widget)
self.detection_count_label = QLabel("Detections: 0")
self.fps_label = QLabel("FPS: 0.0")
self.resolution_label = QLabel("Resolution: N/A")
info_layout.addWidget(self.detection_count_label)
info_layout.addWidget(self.fps_label)
info_layout.addWidget(self.resolution_label)
self.info_widget.setStyleSheet(FinaleStyles.get_group_box_style())
right_panel.addWidget(self.info_widget)
right_panel.addStretch()
layout.addLayout(right_panel, 1)
# Apply theme
self.apply_theme(True)
def update_frame(self, pixmap, detections=None):
"""Update the video frame with detections"""
if pixmap is None:
return
self.current_detections = detections or []
self.video_widget.update_frame(pixmap, self.current_detections)
# Update detection info
self.detection_count_label.setText(f"Detections: {len(self.current_detections)}")
if pixmap:
size = pixmap.size()
self.resolution_label.setText(f"Resolution: {size.width()}x{size.height()}")
def update_fps(self, fps):
"""Update FPS display"""
self.fps_label.setText(f"FPS: {fps:.1f}")
@Slot(bool)
def toggle_overlays(self, enabled):
"""Toggle detection overlays"""
self.video_widget.overlay_enabled = enabled
# Refresh current frame
if self.video_widget.current_pixmap:
self.video_widget.update_frame(self.video_widget.current_pixmap, self.current_detections)
@Slot(int, int)
def on_frame_clicked(self, x, y):
"""Handle frame click for interaction"""
print(f"Frame clicked at ({x}, {y})")
# Could be used for region selection, etc.
def apply_theme(self, dark_mode=True):
"""Apply theme to the view"""
if dark_mode:
self.setStyleSheet(f"""
QWidget {{
background-color: {MaterialColors.surface};
color: {MaterialColors.text_primary};
}}
QPushButton {{
background-color: {MaterialColors.primary};
color: {MaterialColors.text_on_primary};
border: none;
border-radius: 20px;
padding: 8px;
}}
QPushButton:hover {{
background-color: {MaterialColors.primary_variant};
}}
QPushButton:checked {{
background-color: {MaterialColors.secondary};
}}
""")

View File

@@ -0,0 +1,634 @@
"""
Settings View - Application configuration and preferences
Manages all application settings, model configurations, and system preferences.
"""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QGroupBox, QGridLayout, QFrame, QScrollArea, QTabWidget,
QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox, QCheckBox,
QSlider, QTextEdit, QFileDialog, QMessageBox, QProgressBar,
QFormLayout, QButtonGroup, QRadioButton
)
from PySide6.QtCore import Qt, Signal, Slot, QTimer, QSettings, QThread, pyqtSignal
from PySide6.QtGui import QFont, QPixmap
import os
import json
import sys
from pathlib import Path
# Import finale components
from ..styles import FinaleStyles, MaterialColors
from ..icons import FinaleIcons
from qt_app_pyside.ui.config_panel import ConfigPanel
from qt_app_pyside.utils.helpers import load_configuration, save_configuration
from qt_app_pyside.utils.helpers import format_timestamp, format_duration
class ModelConfigWidget(QGroupBox):
"""
Widget for configuring AI models and detection parameters.
"""
config_changed = Signal(dict)
def __init__(self, parent=None):
super().__init__("AI Model Configuration", parent)
self.setup_ui()
def setup_ui(self):
"""Setup model configuration UI"""
layout = QFormLayout(self)
# Vehicle detection model
self.vehicle_model_edit = QLineEdit()
self.vehicle_model_edit.setPlaceholderText("Path to vehicle detection model...")
vehicle_browse_btn = QPushButton(FinaleIcons.get_icon("folder"), "")
vehicle_browse_btn.setFixedSize(32, 32)
vehicle_browse_btn.clicked.connect(lambda: self.browse_model("vehicle"))
vehicle_layout = QHBoxLayout()
vehicle_layout.addWidget(self.vehicle_model_edit)
vehicle_layout.addWidget(vehicle_browse_btn)
layout.addRow("Vehicle Model:", vehicle_layout)
# Traffic light detection model
self.traffic_model_edit = QLineEdit()
self.traffic_model_edit.setPlaceholderText("Path to traffic light model...")
traffic_browse_btn = QPushButton(FinaleIcons.get_icon("folder"), "")
traffic_browse_btn.setFixedSize(32, 32)
traffic_browse_btn.clicked.connect(lambda: self.browse_model("traffic"))
traffic_layout = QHBoxLayout()
traffic_layout.addWidget(self.traffic_model_edit)
traffic_layout.addWidget(traffic_browse_btn)
layout.addRow("Traffic Light Model:", traffic_layout)
# Detection parameters
self.confidence_spin = QDoubleSpinBox()
self.confidence_spin.setRange(0.1, 1.0)
self.confidence_spin.setSingleStep(0.05)
self.confidence_spin.setValue(0.3)
self.confidence_spin.setSuffix(" (30%)")
layout.addRow("Confidence Threshold:", self.confidence_spin)
self.nms_spin = QDoubleSpinBox()
self.nms_spin.setRange(0.1, 1.0)
self.nms_spin.setSingleStep(0.05)
self.nms_spin.setValue(0.45)
layout.addRow("NMS Threshold:", self.nms_spin)
self.max_detections_spin = QSpinBox()
self.max_detections_spin.setRange(10, 1000)
self.max_detections_spin.setValue(100)
layout.addRow("Max Detections:", self.max_detections_spin)
# Device selection
self.device_combo = QComboBox()
self.device_combo.addItems(["CPU", "GPU", "AUTO"])
layout.addRow("Device:", self.device_combo)
# Model optimization
self.optimize_check = QCheckBox("Enable Model Optimization")
self.optimize_check.setChecked(True)
layout.addRow(self.optimize_check)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
@Slot()
def browse_model(self, model_type):
"""Browse for model file"""
file_path, _ = QFileDialog.getOpenFileName(
self, f"Select {model_type.title()} Model", "",
"Model Files (*.xml *.onnx *.pt *.bin);;All Files (*)"
)
if file_path:
if model_type == "vehicle":
self.vehicle_model_edit.setText(file_path)
elif model_type == "traffic":
self.traffic_model_edit.setText(file_path)
def get_config(self):
"""Get current model configuration"""
return {
'vehicle_model': self.vehicle_model_edit.text(),
'traffic_model': self.traffic_model_edit.text(),
'confidence_threshold': self.confidence_spin.value(),
'nms_threshold': self.nms_spin.value(),
'max_detections': self.max_detections_spin.value(),
'device': self.device_combo.currentText(),
'optimize_model': self.optimize_check.isChecked()
}
def set_config(self, config):
"""Set model configuration"""
self.vehicle_model_edit.setText(config.get('vehicle_model', ''))
self.traffic_model_edit.setText(config.get('traffic_model', ''))
self.confidence_spin.setValue(config.get('confidence_threshold', 0.3))
self.nms_spin.setValue(config.get('nms_threshold', 0.45))
self.max_detections_spin.setValue(config.get('max_detections', 100))
self.device_combo.setCurrentText(config.get('device', 'CPU'))
self.optimize_check.setChecked(config.get('optimize_model', True))
class ViolationConfigWidget(QGroupBox):
"""
Widget for configuring violation detection parameters.
"""
def __init__(self, parent=None):
super().__init__("Violation Detection", parent)
self.setup_ui()
def setup_ui(self):
"""Setup violation configuration UI"""
layout = QFormLayout(self)
# Red light violation
self.red_light_check = QCheckBox("Enable Red Light Detection")
self.red_light_check.setChecked(True)
layout.addRow(self.red_light_check)
self.red_light_sensitivity = QSlider(Qt.Horizontal)
self.red_light_sensitivity.setRange(1, 10)
self.red_light_sensitivity.setValue(5)
layout.addRow("Red Light Sensitivity:", self.red_light_sensitivity)
# Speed violation
self.speed_check = QCheckBox("Enable Speed Detection")
self.speed_check.setChecked(True)
layout.addRow(self.speed_check)
self.speed_limit_spin = QSpinBox()
self.speed_limit_spin.setRange(10, 200)
self.speed_limit_spin.setValue(50)
self.speed_limit_spin.setSuffix(" km/h")
layout.addRow("Speed Limit:", self.speed_limit_spin)
self.speed_tolerance_spin = QSpinBox()
self.speed_tolerance_spin.setRange(0, 20)
self.speed_tolerance_spin.setValue(5)
self.speed_tolerance_spin.setSuffix(" km/h")
layout.addRow("Speed Tolerance:", self.speed_tolerance_spin)
# Wrong lane detection
self.wrong_lane_check = QCheckBox("Enable Wrong Lane Detection")
self.wrong_lane_check.setChecked(True)
layout.addRow(self.wrong_lane_check)
# Helmet detection
self.helmet_check = QCheckBox("Enable Helmet Detection")
self.helmet_check.setChecked(False)
layout.addRow(self.helmet_check)
# Violation zone setup
self.zone_setup_btn = QPushButton(FinaleIcons.get_icon("map"), "Setup Violation Zones")
layout.addRow(self.zone_setup_btn)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
class UIPreferencesWidget(QGroupBox):
"""
Widget for UI preferences and appearance settings.
"""
theme_changed = Signal(bool) # dark_mode
def __init__(self, parent=None):
super().__init__("User Interface", parent)
self.setup_ui()
def setup_ui(self):
"""Setup UI preferences"""
layout = QFormLayout(self)
# Theme selection
theme_group = QButtonGroup(self)
self.dark_radio = QRadioButton("Dark Theme")
self.light_radio = QRadioButton("Light Theme")
self.auto_radio = QRadioButton("Auto (System)")
self.dark_radio.setChecked(True) # Default to dark
theme_group.addButton(self.dark_radio)
theme_group.addButton(self.light_radio)
theme_group.addButton(self.auto_radio)
theme_layout = QVBoxLayout()
theme_layout.addWidget(self.dark_radio)
theme_layout.addWidget(self.light_radio)
theme_layout.addWidget(self.auto_radio)
layout.addRow("Theme:", theme_layout)
# Language selection
self.language_combo = QComboBox()
self.language_combo.addItems(["English", "Español", "Français", "Deutsch", "العربية"])
layout.addRow("Language:", self.language_combo)
# Font size
self.font_size_spin = QSpinBox()
self.font_size_spin.setRange(8, 16)
self.font_size_spin.setValue(9)
layout.addRow("Font Size:", self.font_size_spin)
# Animations
self.animations_check = QCheckBox("Enable Animations")
self.animations_check.setChecked(True)
layout.addRow(self.animations_check)
# Sound notifications
self.sound_check = QCheckBox("Sound Notifications")
self.sound_check.setChecked(True)
layout.addRow(self.sound_check)
# Auto-save
self.autosave_check = QCheckBox("Auto-save Configuration")
self.autosave_check.setChecked(True)
layout.addRow(self.autosave_check)
# Update interval
self.update_interval_spin = QSpinBox()
self.update_interval_spin.setRange(100, 5000)
self.update_interval_spin.setValue(1000)
self.update_interval_spin.setSuffix(" ms")
layout.addRow("Update Interval:", self.update_interval_spin)
# Connect theme signals
self.dark_radio.toggled.connect(lambda checked: self.theme_changed.emit(True) if checked else None)
self.light_radio.toggled.connect(lambda checked: self.theme_changed.emit(False) if checked else None)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
class PerformanceWidget(QGroupBox):
"""
Widget for performance and system settings.
"""
def __init__(self, parent=None):
super().__init__("Performance", parent)
self.setup_ui()
def setup_ui(self):
"""Setup performance settings"""
layout = QFormLayout(self)
# Processing threads
self.threads_spin = QSpinBox()
self.threads_spin.setRange(1, 16)
self.threads_spin.setValue(4)
layout.addRow("Processing Threads:", self.threads_spin)
# Frame buffer size
self.buffer_size_spin = QSpinBox()
self.buffer_size_spin.setRange(1, 100)
self.buffer_size_spin.setValue(10)
layout.addRow("Frame Buffer Size:", self.buffer_size_spin)
# Memory limit
self.memory_limit_spin = QSpinBox()
self.memory_limit_spin.setRange(512, 8192)
self.memory_limit_spin.setValue(2048)
self.memory_limit_spin.setSuffix(" MB")
layout.addRow("Memory Limit:", self.memory_limit_spin)
# GPU acceleration
self.gpu_check = QCheckBox("Enable GPU Acceleration")
self.gpu_check.setChecked(False)
layout.addRow(self.gpu_check)
# Performance mode
self.performance_combo = QComboBox()
self.performance_combo.addItems(["Balanced", "Performance", "Power Save"])
layout.addRow("Performance Mode:", self.performance_combo)
# Logging level
self.logging_combo = QComboBox()
self.logging_combo.addItems(["DEBUG", "INFO", "WARNING", "ERROR"])
self.logging_combo.setCurrentText("INFO")
layout.addRow("Logging Level:", self.logging_combo)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
class DataManagementWidget(QGroupBox):
"""
Widget for data storage and export settings.
"""
def __init__(self, parent=None):
super().__init__("Data Management", parent)
self.setup_ui()
def setup_ui(self):
"""Setup data management settings"""
layout = QFormLayout(self)
# Data directory
self.data_dir_edit = QLineEdit()
self.data_dir_edit.setPlaceholderText("Data storage directory...")
data_browse_btn = QPushButton(FinaleIcons.get_icon("folder"), "")
data_browse_btn.setFixedSize(32, 32)
data_browse_btn.clicked.connect(self.browse_data_directory)
data_layout = QHBoxLayout()
data_layout.addWidget(self.data_dir_edit)
data_layout.addWidget(data_browse_btn)
layout.addRow("Data Directory:", data_layout)
# Auto-export
self.auto_export_check = QCheckBox("Auto-export Violations")
layout.addRow(self.auto_export_check)
# Export format
self.export_format_combo = QComboBox()
self.export_format_combo.addItems(["JSON", "CSV", "XML", "PDF"])
layout.addRow("Export Format:", self.export_format_combo)
# Data retention
self.retention_spin = QSpinBox()
self.retention_spin.setRange(1, 365)
self.retention_spin.setValue(30)
self.retention_spin.setSuffix(" days")
layout.addRow("Data Retention:", self.retention_spin)
# Backup settings
self.backup_check = QCheckBox("Enable Automatic Backup")
layout.addRow(self.backup_check)
self.backup_interval_combo = QComboBox()
self.backup_interval_combo.addItems(["Daily", "Weekly", "Monthly"])
layout.addRow("Backup Interval:", self.backup_interval_combo)
# Database cleanup
cleanup_btn = QPushButton(FinaleIcons.get_icon("delete"), "Cleanup Old Data")
layout.addRow(cleanup_btn)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
@Slot()
def browse_data_directory(self):
"""Browse for data directory"""
directory = QFileDialog.getExistingDirectory(
self, "Select Data Directory", self.data_dir_edit.text()
)
if directory:
self.data_dir_edit.setText(directory)
class SettingsView(QWidget):
"""
Main settings view with tabbed configuration sections.
"""
settings_changed = Signal(dict)
def __init__(self, parent=None):
super().__init__(parent)
self.config = load_configuration('config.json')
# Add configuration panel from original
self.config_panel = ConfigPanel()
self.settings = QSettings("Finale", "TrafficMonitoring")
self.setup_ui()
self.load_settings()
def setup_ui(self):
"""Setup the settings view UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(16)
# Header
header_layout = QHBoxLayout()
title_label = QLabel("Settings")
title_label.setFont(QFont("Segoe UI", 18, QFont.Bold))
# Action buttons
self.reset_btn = QPushButton(FinaleIcons.get_icon("refresh"), "Reset to Defaults")
self.reset_btn.clicked.connect(self.reset_to_defaults)
self.export_btn = QPushButton(FinaleIcons.get_icon("export"), "Export Settings")
self.export_btn.clicked.connect(self.export_settings)
self.import_btn = QPushButton(FinaleIcons.get_icon("import"), "Import Settings")
self.import_btn.clicked.connect(self.import_settings)
header_layout.addWidget(title_label)
header_layout.addStretch()
header_layout.addWidget(self.reset_btn)
header_layout.addWidget(self.export_btn)
header_layout.addWidget(self.import_btn)
layout.addLayout(header_layout)
# Settings tabs
self.tabs = QTabWidget()
# Create configuration widgets
self.model_config = ModelConfigWidget()
self.violation_config = ViolationConfigWidget()
self.ui_preferences = UIPreferencesWidget()
self.performance_config = PerformanceWidget()
self.data_management = DataManagementWidget()
# Add tabs
self.tabs.addTab(self.model_config, FinaleIcons.get_icon("model"), "AI Models")
self.tabs.addTab(self.violation_config, FinaleIcons.get_icon("warning"), "Violations")
self.tabs.addTab(self.ui_preferences, FinaleIcons.get_icon("palette"), "Interface")
self.tabs.addTab(self.performance_config, FinaleIcons.get_icon("speed"), "Performance")
self.tabs.addTab(self.data_management, FinaleIcons.get_icon("database"), "Data")
# Style tabs
self.tabs.setStyleSheet(FinaleStyles.get_tab_widget_style())
layout.addWidget(self.tabs, 1)
# Bottom action bar
action_layout = QHBoxLayout()
self.apply_btn = QPushButton(FinaleIcons.get_icon("check"), "Apply")
self.apply_btn.clicked.connect(self.apply_settings)
self.save_btn = QPushButton(FinaleIcons.get_icon("save"), "Save")
self.save_btn.clicked.connect(self.save_settings)
self.cancel_btn = QPushButton(FinaleIcons.get_icon("close"), "Cancel")
self.cancel_btn.clicked.connect(self.cancel_changes)
action_layout.addStretch()
action_layout.addWidget(self.apply_btn)
action_layout.addWidget(self.save_btn)
action_layout.addWidget(self.cancel_btn)
layout.addLayout(action_layout)
# Connect signals
self.ui_preferences.theme_changed.connect(self.on_theme_changed)
# Apply theme
self.apply_theme(True)
def load_settings(self):
"""Load settings from QSettings"""
# Load model configuration
model_config = {
'vehicle_model': self.settings.value('model/vehicle_model', ''),
'traffic_model': self.settings.value('model/traffic_model', ''),
'confidence_threshold': self.settings.value('model/confidence_threshold', 0.3, float),
'nms_threshold': self.settings.value('model/nms_threshold', 0.45, float),
'max_detections': self.settings.value('model/max_detections', 100, int),
'device': self.settings.value('model/device', 'CPU'),
'optimize_model': self.settings.value('model/optimize_model', True, bool)
}
self.model_config.set_config(model_config)
# Load UI preferences
dark_mode = self.settings.value('ui/dark_mode', True, bool)
if dark_mode:
self.ui_preferences.dark_radio.setChecked(True)
else:
self.ui_preferences.light_radio.setChecked(True)
@Slot()
def apply_settings(self):
"""Apply current settings"""
settings_data = self.get_all_settings()
self.settings_changed.emit(settings_data)
@Slot()
def save_settings(self):
"""Save settings to QSettings"""
# Save model configuration
model_config = self.model_config.get_config()
for key, value in model_config.items():
self.settings.setValue(f'model/{key}', value)
# Save UI preferences
self.settings.setValue('ui/dark_mode', self.ui_preferences.dark_radio.isChecked())
# Sync settings
self.settings.sync()
QMessageBox.information(self, "Settings Saved", "Settings have been saved successfully.")
save_configuration(settings_data, 'config.json')
@Slot()
def cancel_changes(self):
"""Cancel changes and reload settings"""
self.load_settings()
@Slot()
def reset_to_defaults(self):
"""Reset all settings to defaults"""
reply = QMessageBox.question(
self, "Reset Settings",
"Are you sure you want to reset all settings to defaults?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self.settings.clear()
self.load_settings()
@Slot()
def export_settings(self):
"""Export settings to file"""
file_path, _ = QFileDialog.getSaveFileName(
self, "Export Settings", "",
"JSON Files (*.json);;All Files (*)"
)
if file_path:
settings_data = self.get_all_settings()
try:
with open(file_path, 'w') as f:
json.dump(settings_data, f, indent=2)
QMessageBox.information(self, "Export Successful", "Settings exported successfully.")
except Exception as e:
QMessageBox.critical(self, "Export Error", f"Failed to export settings:\n{str(e)}")
@Slot()
def import_settings(self):
"""Import settings from file"""
file_path, _ = QFileDialog.getOpenFileName(
self, "Import Settings", "",
"JSON Files (*.json);;All Files (*)"
)
if file_path:
try:
with open(file_path, 'r') as f:
settings_data = json.load(f)
# Apply imported settings
self.apply_imported_settings(settings_data)
QMessageBox.information(self, "Import Successful", "Settings imported successfully.")
except Exception as e:
QMessageBox.critical(self, "Import Error", f"Failed to import settings:\n{str(e)}")
def get_all_settings(self):
"""Get all current settings as dictionary"""
return {
'model': self.model_config.get_config(),
'ui': {
'dark_mode': self.ui_preferences.dark_radio.isChecked(),
'language': self.ui_preferences.language_combo.currentText(),
'font_size': self.ui_preferences.font_size_spin.value(),
'animations': self.ui_preferences.animations_check.isChecked(),
'sound': self.ui_preferences.sound_check.isChecked()
}
}
def apply_imported_settings(self, settings_data):
"""Apply imported settings data"""
if 'model' in settings_data:
self.model_config.set_config(settings_data['model'])
if 'ui' in settings_data:
ui_settings = settings_data['ui']
if 'dark_mode' in ui_settings:
if ui_settings['dark_mode']:
self.ui_preferences.dark_radio.setChecked(True)
else:
self.ui_preferences.light_radio.setChecked(True)
@Slot(bool)
def on_theme_changed(self, dark_mode):
"""Handle theme change"""
self.apply_theme(dark_mode)
def apply_theme(self, dark_mode=True):
"""Apply theme to the view"""
if dark_mode:
self.setStyleSheet(f"""
QWidget {{
background-color: {MaterialColors.surface};
color: {MaterialColors.text_primary};
}}
QPushButton {{
background-color: {MaterialColors.primary};
color: {MaterialColors.text_on_primary};
border: none;
border-radius: 6px;
padding: 8px 16px;
}}
QPushButton:hover {{
background-color: {MaterialColors.primary_variant};
}}
""")
def display_timestamp(self, ts):
return format_timestamp(ts)
def display_duration(self, seconds):
return format_duration(seconds)

View File

@@ -0,0 +1,609 @@
"""
Violations View - Violation management and history
Displays violation records, details, and management tools.
"""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QGroupBox, QGridLayout, QFrame, QScrollArea, QTabWidget,
QTableWidget, QTableWidgetItem, QHeaderView, QDateEdit,
QComboBox, QSpinBox, QLineEdit, QTextEdit, QDialog,
QDialogButtonBox, QSplitter, QListWidget, QListWidgetItem
)
from PySide6.QtCore import Qt, Signal, Slot, QTimer, QDate, QSize
from PySide6.QtGui import QPixmap, QPainter, QBrush, QColor, QFont, QIcon
from datetime import datetime, timedelta
import json
import os
# Import finale components
from ..styles import FinaleStyles, MaterialColors
from ..icons import FinaleIcons
from qt_app_pyside.utils.helpers import save_configuration, create_export_csv, create_export_json
from qt_app_pyside.utils.annotation_utils import draw_detections
from qt_app_pyside.utils.enhanced_annotation_utils import enhanced_draw_detections
from qt_app_pyside.ui.export_tab import ExportTab
from qt_app_pyside.ui.violations_tab import ViolationsTab as OriginalViolationsTab
class ViolationDetailDialog(QDialog):
"""
Dialog for viewing detailed violation information.
"""
def __init__(self, violation_data, parent=None):
super().__init__(parent)
self.violation_data = violation_data
self.setup_ui()
def setup_ui(self):
"""Setup the detail dialog UI"""
self.setWindowTitle("Violation Details")
self.setMinimumSize(600, 500)
layout = QVBoxLayout(self)
# Header with violation type and timestamp
header_frame = QFrame()
header_frame.setStyleSheet(f"""
QFrame {{
background-color: {MaterialColors.primary};
color: {MaterialColors.text_on_primary};
border-radius: 8px;
padding: 16px;
}}
""")
header_layout = QHBoxLayout(header_frame)
violation_type = self.violation_data.get('type', 'Unknown')
timestamp = self.violation_data.get('timestamp', 'Unknown')
type_label = QLabel(violation_type)
type_label.setFont(QFont("Segoe UI", 16, QFont.Bold))
time_label = QLabel(timestamp)
time_label.setFont(QFont("Segoe UI", 12))
header_layout.addWidget(type_label)
header_layout.addStretch()
header_layout.addWidget(time_label)
layout.addWidget(header_frame)
# Main content area
content_splitter = QSplitter(Qt.Horizontal)
# Left side - Image/Video
image_group = QGroupBox("Evidence")
image_layout = QVBoxLayout(image_group)
self.image_label = QLabel()
self.image_label.setMinimumSize(300, 200)
self.image_label.setStyleSheet("""
QLabel {
border: 2px solid #424242;
border-radius: 8px;
background-color: #1a1a1a;
}
""")
self.image_label.setAlignment(Qt.AlignCenter)
self.image_label.setText("No image available")
# Load image if available
image_path = self.violation_data.get('image_path')
if image_path and os.path.exists(image_path):
pixmap = QPixmap(image_path)
if not pixmap.isNull():
scaled_pixmap = pixmap.scaled(300, 200, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.image_label.setPixmap(scaled_pixmap)
image_layout.addWidget(self.image_label)
# Image controls
image_controls = QHBoxLayout()
save_image_btn = QPushButton(FinaleIcons.get_icon("save"), "Save Image")
view_full_btn = QPushButton(FinaleIcons.get_icon("zoom_in"), "View Full")
image_controls.addWidget(save_image_btn)
image_controls.addWidget(view_full_btn)
image_controls.addStretch()
image_layout.addLayout(image_controls)
content_splitter.addWidget(image_group)
# Right side - Details
details_group = QGroupBox("Details")
details_layout = QGridLayout(details_group)
# Violation details
details = [
("Vehicle ID:", self.violation_data.get('vehicle_id', 'Unknown')),
("Location:", self.violation_data.get('location', 'Unknown')),
("Confidence:", f"{self.violation_data.get('confidence', 0.0):.2f}"),
("Speed:", f"{self.violation_data.get('speed', 0.0):.1f} km/h"),
("Lane:", self.violation_data.get('lane', 'Unknown')),
("Weather:", self.violation_data.get('weather', 'Unknown')),
("Officer ID:", self.violation_data.get('officer_id', 'N/A')),
("Status:", self.violation_data.get('status', 'Pending'))
]
for i, (label, value) in enumerate(details):
label_widget = QLabel(label)
label_widget.setFont(QFont("Segoe UI", 9, QFont.Bold))
value_widget = QLabel(str(value))
value_widget.setStyleSheet(f"color: {MaterialColors.text_secondary};")
details_layout.addWidget(label_widget, i, 0)
details_layout.addWidget(value_widget, i, 1)
# Notes section
notes_label = QLabel("Notes:")
notes_label.setFont(QFont("Segoe UI", 9, QFont.Bold))
details_layout.addWidget(notes_label, len(details), 0, 1, 2)
self.notes_edit = QTextEdit()
self.notes_edit.setMaximumHeight(100)
self.notes_edit.setPlainText(self.violation_data.get('notes', ''))
details_layout.addWidget(self.notes_edit, len(details) + 1, 0, 1, 2)
content_splitter.addWidget(details_group)
layout.addWidget(content_splitter)
# Action buttons
button_layout = QHBoxLayout()
export_btn = QPushButton(FinaleIcons.get_icon("export"), "Export Report")
delete_btn = QPushButton(FinaleIcons.get_icon("delete"), "Delete")
delete_btn.setStyleSheet(f"background-color: {MaterialColors.error};")
button_layout.addWidget(export_btn)
button_layout.addWidget(delete_btn)
button_layout.addStretch()
# Standard dialog buttons
button_box = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Close)
button_box.accepted.connect(self.save_changes)
button_box.rejected.connect(self.reject)
button_layout.addWidget(button_box)
layout.addLayout(button_layout)
# Apply styling
self.setStyleSheet(FinaleStyles.get_dialog_style())
@Slot()
def save_changes(self):
"""Save changes to violation data"""
# Update notes
self.violation_data['notes'] = self.notes_edit.toPlainText()
# Here you would save to database/file
self.accept()
class ViolationFilterWidget(QGroupBox):
"""
Widget for filtering violations by various criteria.
"""
filter_changed = Signal(dict)
def __init__(self, parent=None):
super().__init__("Filter Violations", parent)
self.setup_ui()
def setup_ui(self):
"""Setup filter UI"""
layout = QGridLayout(self)
# Date range
layout.addWidget(QLabel("Date From:"), 0, 0)
self.date_from = QDateEdit()
self.date_from.setDate(QDate.currentDate().addDays(-30))
self.date_from.setCalendarPopup(True)
layout.addWidget(self.date_from, 0, 1)
layout.addWidget(QLabel("Date To:"), 0, 2)
self.date_to = QDateEdit()
self.date_to.setDate(QDate.currentDate())
self.date_to.setCalendarPopup(True)
layout.addWidget(self.date_to, 0, 3)
# Violation type
layout.addWidget(QLabel("Type:"), 1, 0)
self.type_combo = QComboBox()
self.type_combo.addItems(["All Types", "Red Light", "Speed", "Wrong Lane", "No Helmet", "Other"])
layout.addWidget(self.type_combo, 1, 1)
# Status
layout.addWidget(QLabel("Status:"), 1, 2)
self.status_combo = QComboBox()
self.status_combo.addItems(["All Status", "Pending", "Reviewed", "Closed", "Disputed"])
layout.addWidget(self.status_combo, 1, 3)
# Location
layout.addWidget(QLabel("Location:"), 2, 0)
self.location_edit = QLineEdit()
self.location_edit.setPlaceholderText("Enter location...")
layout.addWidget(self.location_edit, 2, 1)
# Confidence threshold
layout.addWidget(QLabel("Min Confidence:"), 2, 2)
self.confidence_spin = QSpinBox()
self.confidence_spin.setRange(0, 100)
self.confidence_spin.setValue(50)
self.confidence_spin.setSuffix("%")
layout.addWidget(self.confidence_spin, 2, 3)
# Apply button
self.apply_btn = QPushButton(FinaleIcons.get_icon("filter"), "Apply Filter")
self.apply_btn.clicked.connect(self.apply_filter)
layout.addWidget(self.apply_btn, 3, 0, 1, 4)
# Connect signals for auto-update
self.date_from.dateChanged.connect(self.on_filter_changed)
self.date_to.dateChanged.connect(self.on_filter_changed)
self.type_combo.currentTextChanged.connect(self.on_filter_changed)
self.status_combo.currentTextChanged.connect(self.on_filter_changed)
# Apply styling
self.setStyleSheet(FinaleStyles.get_group_box_style())
@Slot()
def apply_filter(self):
"""Apply current filter settings"""
self.on_filter_changed()
def on_filter_changed(self):
"""Emit filter changed signal with current settings"""
filter_data = {
'date_from': self.date_from.date().toPython(),
'date_to': self.date_to.date().toPython(),
'type': self.type_combo.currentText(),
'status': self.status_combo.currentText(),
'location': self.location_edit.text(),
'min_confidence': self.confidence_spin.value() / 100.0
}
self.filter_changed.emit(filter_data)
class ViolationListWidget(QWidget):
"""
Widget displaying violation list with thumbnails and quick info.
"""
violation_selected = Signal(dict)
def __init__(self, parent=None):
super().__init__(parent)
self.violations = []
self.setup_ui()
def setup_ui(self):
"""Setup violation list UI"""
layout = QVBoxLayout(self)
# Header
header_layout = QHBoxLayout()
self.count_label = QLabel("0 violations")
self.count_label.setFont(QFont("Segoe UI", 12, QFont.Bold))
self.sort_combo = QComboBox()
self.sort_combo.addItems(["Sort by Time", "Sort by Type", "Sort by Confidence", "Sort by Status"])
self.sort_combo.currentTextChanged.connect(self.sort_violations)
header_layout.addWidget(self.count_label)
header_layout.addStretch()
header_layout.addWidget(QLabel("Sort:"))
header_layout.addWidget(self.sort_combo)
layout.addLayout(header_layout)
# Violations list
self.list_widget = QListWidget()
self.list_widget.itemClicked.connect(self.on_item_clicked)
self.list_widget.setStyleSheet(FinaleStyles.get_list_style())
layout.addWidget(self.list_widget)
def add_violation(self, violation_data):
"""Add a violation to the list"""
self.violations.append(violation_data)
self.update_list()
def set_violations(self, violations):
"""Set the complete list of violations"""
self.violations = violations
self.update_list()
def update_list(self):
"""Update the violation list display"""
self.list_widget.clear()
for violation in self.violations:
item = QListWidgetItem()
# Create custom widget for violation item
item_widget = self.create_violation_item_widget(violation)
item.setSizeHint(item_widget.sizeHint())
self.list_widget.addItem(item)
self.list_widget.setItemWidget(item, item_widget)
# Update count
self.count_label.setText(f"{len(self.violations)} violations")
def create_violation_item_widget(self, violation):
"""Create a custom widget for a violation list item"""
widget = QWidget()
layout = QHBoxLayout(widget)
layout.setContentsMargins(8, 8, 8, 8)
# Thumbnail (placeholder for now)
thumbnail = QLabel()
thumbnail.setFixedSize(80, 60)
thumbnail.setStyleSheet("""
QLabel {
border: 1px solid #424242;
border-radius: 4px;
background-color: #2d2d2d;
}
""")
thumbnail.setAlignment(Qt.AlignCenter)
thumbnail.setText("IMG")
layout.addWidget(thumbnail)
# Violation info
info_layout = QVBoxLayout()
# Title line
title_layout = QHBoxLayout()
type_label = QLabel(violation.get('type', 'Unknown'))
type_label.setFont(QFont("Segoe UI", 11, QFont.Bold))
time_label = QLabel(violation.get('timestamp', ''))
time_label.setStyleSheet(f"color: {MaterialColors.text_secondary}; font-size: 10px;")
title_layout.addWidget(type_label)
title_layout.addStretch()
title_layout.addWidget(time_label)
info_layout.addLayout(title_layout)
# Details line
details = f"Vehicle: {violation.get('vehicle_id', 'Unknown')} | Location: {violation.get('location', 'Unknown')}"
details_label = QLabel(details)
details_label.setStyleSheet(f"color: {MaterialColors.text_secondary}; font-size: 9px;")
info_layout.addWidget(details_label)
# Confidence and status
status_layout = QHBoxLayout()
confidence = violation.get('confidence', 0.0)
confidence_label = QLabel(f"Confidence: {confidence:.2f}")
confidence_label.setStyleSheet(f"color: {MaterialColors.primary}; font-size: 9px;")
status = violation.get('status', 'Pending')
status_label = QLabel(status)
status_color = {
'Pending': MaterialColors.warning,
'Reviewed': MaterialColors.primary,
'Closed': MaterialColors.success,
'Disputed': MaterialColors.error
}.get(status, MaterialColors.text_secondary)
status_label.setStyleSheet(f"color: {status_color}; font-size: 9px; font-weight: bold;")
status_layout.addWidget(confidence_label)
status_layout.addStretch()
status_layout.addWidget(status_label)
info_layout.addLayout(status_layout)
layout.addLayout(info_layout, 1)
# Store violation data in widget
widget.violation_data = violation
return widget
def sort_violations(self, sort_by):
"""Sort violations by the specified criteria"""
if sort_by == "Sort by Time":
self.violations.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
elif sort_by == "Sort by Type":
self.violations.sort(key=lambda x: x.get('type', ''))
elif sort_by == "Sort by Confidence":
self.violations.sort(key=lambda x: x.get('confidence', 0.0), reverse=True)
elif sort_by == "Sort by Status":
self.violations.sort(key=lambda x: x.get('status', ''))
self.update_list()
@Slot(QListWidgetItem)
def on_item_clicked(self, item):
"""Handle violation item click"""
item_widget = self.list_widget.itemWidget(item)
if hasattr(item_widget, 'violation_data'):
self.violation_selected.emit(item_widget.violation_data)
class ViolationsView(QWidget):
"""
Main violations view with filtering, list, and detail management.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setup_ui()
self.load_sample_data()
self.save_config = save_configuration
self.export_csv = create_export_csv
self.export_json = create_export_json
self.draw_detections = draw_detections
self.enhanced_draw_detections = enhanced_draw_detections
# Add export functionality from original export_tab
self.export_handler = ExportTab()
def setup_ui(self):
"""Setup the violations view UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(16, 16, 16, 16)
layout.setSpacing(16)
# Filter widget
self.filter_widget = ViolationFilterWidget()
self.filter_widget.filter_changed.connect(self.apply_filter)
layout.addWidget(self.filter_widget)
# Main content area
content_splitter = QSplitter(Qt.Horizontal)
# Left side - Violation list
self.violation_list = ViolationListWidget()
self.violation_list.violation_selected.connect(self.show_violation_details)
content_splitter.addWidget(self.violation_list)
# Right side - Quick actions and summary
right_panel = QWidget()
right_layout = QVBoxLayout(right_panel)
# Quick actions
actions_group = QGroupBox("Quick Actions")
actions_layout = QVBoxLayout(actions_group)
export_all_btn = QPushButton(FinaleIcons.get_icon("export"), "Export All")
export_filtered_btn = QPushButton(FinaleIcons.get_icon("filter"), "Export Filtered")
delete_selected_btn = QPushButton(FinaleIcons.get_icon("delete"), "Delete Selected")
mark_reviewed_btn = QPushButton(FinaleIcons.get_icon("check"), "Mark as Reviewed")
actions_layout.addWidget(export_all_btn)
actions_layout.addWidget(export_filtered_btn)
actions_layout.addWidget(delete_selected_btn)
actions_layout.addWidget(mark_reviewed_btn)
actions_group.setStyleSheet(FinaleStyles.get_group_box_style())
right_layout.addWidget(actions_group)
# Summary statistics
summary_group = QGroupBox("Summary")
summary_layout = QGridLayout(summary_group)
self.total_label = QLabel("Total: 0")
self.pending_label = QLabel("Pending: 0")
self.reviewed_label = QLabel("Reviewed: 0")
self.closed_label = QLabel("Closed: 0")
summary_layout.addWidget(self.total_label, 0, 0)
summary_layout.addWidget(self.pending_label, 0, 1)
summary_layout.addWidget(self.reviewed_label, 1, 0)
summary_layout.addWidget(self.closed_label, 1, 1)
summary_group.setStyleSheet(FinaleStyles.get_group_box_style())
right_layout.addWidget(summary_group)
right_layout.addStretch()
content_splitter.addWidget(right_panel)
# Set splitter proportions
content_splitter.setSizes([700, 300])
layout.addWidget(content_splitter, 1)
# Apply theme
self.apply_theme(True)
def load_sample_data(self):
"""Load sample violation data for demonstration"""
sample_violations = [
{
'timestamp': '14:23:15',
'type': 'Red Light',
'vehicle_id': 'VH1234',
'location': 'Main St & 1st Ave',
'confidence': 0.92,
'status': 'Pending',
'speed': 45.2,
'lane': 'Left Turn',
'notes': 'Clear violation captured on camera.'
},
{
'timestamp': '13:45:32',
'type': 'Speed',
'vehicle_id': 'VH5678',
'location': 'Highway 101',
'confidence': 0.87,
'status': 'Reviewed',
'speed': 78.5,
'lane': 'Right',
'notes': 'Speed limit 60 km/h, vehicle traveling at 78.5 km/h.'
},
{
'timestamp': '12:15:48',
'type': 'Wrong Lane',
'vehicle_id': 'VH9012',
'location': 'Oak St Bridge',
'confidence': 0.76,
'status': 'Closed',
'speed': 32.1,
'lane': 'Bus Lane',
'notes': 'Vehicle in bus-only lane during restricted hours.'
}
]
self.violation_list.set_violations(sample_violations)
self.update_summary()
def add_violation(self, violation_data):
"""Add a new violation (called from main window)"""
self.violation_list.add_violation(violation_data)
self.update_summary()
@Slot(dict)
def apply_filter(self, filter_data):
"""Apply filter to violation list"""
print(f"Applying filter: {filter_data}")
# Here you would filter the violations based on criteria
# For now, just update summary
self.update_summary()
@Slot(dict)
def show_violation_details(self, violation_data):
"""Show detailed view of selected violation"""
dialog = ViolationDetailDialog(violation_data, self)
dialog.exec()
def update_summary(self):
"""Update summary statistics"""
violations = self.violation_list.violations
total = len(violations)
pending = len([v for v in violations if v.get('status') == 'Pending'])
reviewed = len([v for v in violations if v.get('status') == 'Reviewed'])
closed = len([v for v in violations if v.get('status') == 'Closed'])
self.total_label.setText(f"Total: {total}")
self.pending_label.setText(f"Pending: {pending}")
self.reviewed_label.setText(f"Reviewed: {reviewed}")
self.closed_label.setText(f"Closed: {closed}")
def apply_theme(self, dark_mode=True):
"""Apply theme to the view"""
if dark_mode:
self.setStyleSheet(f"""
QWidget {{
background-color: {MaterialColors.surface};
color: {MaterialColors.text_primary};
}}
QPushButton {{
background-color: {MaterialColors.primary};
color: {MaterialColors.text_on_primary};
border: none;
border-radius: 6px;
padding: 8px 16px;
}}
QPushButton:hover {{
background-color: {MaterialColors.primary_variant};
}}
""")