1577 lines
56 KiB
Python
1577 lines
56 KiB
Python
"""
|
|
Advanced UI Design for Traffic Intersection Monitoring System
|
|
============================================================
|
|
|
|
This module implements a modern, dark-themed UI with Material Design principles
|
|
featuring tabbed navigation, live statistics, violation logs, and animated transitions.
|
|
|
|
Design Language:
|
|
- Dark theme (#121212, #1E1E1E backgrounds)
|
|
- Material Design with accent colors (green, red, yellow)
|
|
- Rounded corners, subtle shadows, elevation
|
|
- Animated transitions and responsive interactions
|
|
- Consistent typography (Segoe UI/Inter/Roboto)
|
|
- Icon-based navigation and controls
|
|
|
|
Author: Traffic Monitoring System
|
|
Date: July 2025
|
|
"""
|
|
|
|
import sys
|
|
from datetime import datetime
|
|
from typing import Optional, Dict, List, Any
|
|
import json
|
|
|
|
from PySide6.QtWidgets import (
|
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QTabWidget, QLabel, QPushButton, QSlider, QCheckBox, QComboBox,
|
|
QTableWidget, QTableWidgetItem, QFrame, QProgressBar, QTextEdit,
|
|
QSplitter, QGroupBox, QGridLayout, QSpacerItem, QSizePolicy,
|
|
QScrollArea, QStackedWidget, QToolBar, QStatusBar, QMenuBar,
|
|
QMenu, QAction, QFileDialog, QMessageBox, QDialog, QDialogButtonBox,
|
|
QFormLayout, QLineEdit, QSpinBox, QDoubleSpinBox, QHeaderView
|
|
)
|
|
|
|
from PySide6.QtCore import (
|
|
Qt, QTimer, QPropertyAnimation, QEasingCurve, QRect, QSize,
|
|
QThread, Signal, QObject, QParallelAnimationGroup, QSequentialAnimationGroup
|
|
)
|
|
|
|
from PySide6.QtGui import (
|
|
QFont, QPixmap, QPainter, QPalette, QColor, QBrush, QLinearGradient,
|
|
QIcon, QAction, QKeySequence, QPen, QFontMetrics
|
|
)
|
|
|
|
try:
|
|
import pyqtgraph as pg
|
|
PYQTGRAPH_AVAILABLE = True
|
|
except ImportError:
|
|
PYQTGRAPH_AVAILABLE = False
|
|
print("PyQtGraph not available. Charts will be disabled.")
|
|
|
|
|
|
class MaterialColors:
|
|
"""Material Design color palette for dark theme"""
|
|
|
|
# Background colors
|
|
BACKGROUND_PRIMARY = "#121212"
|
|
BACKGROUND_SECONDARY = "#1E1E1E"
|
|
BACKGROUND_TERTIARY = "#2D2D2D"
|
|
|
|
# Surface colors
|
|
SURFACE = "#1E1E1E"
|
|
SURFACE_VARIANT = "#323232"
|
|
|
|
# Accent colors
|
|
PRIMARY = "#00BCD4" # Cyan
|
|
PRIMARY_VARIANT = "#00ACC1"
|
|
SECONDARY = "#FFC107" # Amber
|
|
SECONDARY_VARIANT = "#FFB300"
|
|
|
|
# Status colors
|
|
SUCCESS = "#4CAF50" # Green
|
|
WARNING = "#FF9800" # Orange
|
|
ERROR = "#F44336" # Red
|
|
INFO = "#2196F3" # Blue
|
|
|
|
# Text colors
|
|
TEXT_PRIMARY = "#FFFFFF"
|
|
TEXT_SECONDARY = "#B0B0B0"
|
|
TEXT_DISABLED = "#666666"
|
|
|
|
# Border colors
|
|
BORDER = "#404040"
|
|
BORDER_LIGHT = "#606060"
|
|
|
|
|
|
class AnimationHelper:
|
|
"""Helper class for creating smooth animations"""
|
|
|
|
@staticmethod
|
|
def create_fade_animation(widget, duration=300, start_opacity=0.0, end_opacity=1.0):
|
|
"""Create fade in/out animation"""
|
|
animation = QPropertyAnimation(widget, b"windowOpacity")
|
|
animation.setDuration(duration)
|
|
animation.setStartValue(start_opacity)
|
|
animation.setEndValue(end_opacity)
|
|
animation.setEasingCurve(QEasingCurve.Type.OutCubic)
|
|
return animation
|
|
|
|
@staticmethod
|
|
def create_slide_animation(widget, duration=300, start_pos=None, end_pos=None):
|
|
"""Create slide animation"""
|
|
animation = QPropertyAnimation(widget, b"geometry")
|
|
animation.setDuration(duration)
|
|
if start_pos:
|
|
animation.setStartValue(QRect(*start_pos))
|
|
if end_pos:
|
|
animation.setEndValue(QRect(*end_pos))
|
|
animation.setEasingCurve(QEasingCurve.Type.OutCubic)
|
|
return animation
|
|
|
|
|
|
class ModernButton(QPushButton):
|
|
"""Custom button with modern styling and animations"""
|
|
|
|
def __init__(self, text="", icon=None, button_type="primary", parent=None):
|
|
super().__init__(text, parent)
|
|
self.button_type = button_type
|
|
self.setup_style()
|
|
|
|
if icon:
|
|
self.setIcon(icon)
|
|
self.setIconSize(QSize(16, 16))
|
|
|
|
def setup_style(self):
|
|
"""Apply modern button styling"""
|
|
if self.button_type == "primary":
|
|
bg_color = MaterialColors.PRIMARY
|
|
hover_color = MaterialColors.PRIMARY_VARIANT
|
|
elif self.button_type == "success":
|
|
bg_color = MaterialColors.SUCCESS
|
|
hover_color = "#45A049"
|
|
elif self.button_type == "warning":
|
|
bg_color = MaterialColors.WARNING
|
|
hover_color = "#E68900"
|
|
elif self.button_type == "error":
|
|
bg_color = MaterialColors.ERROR
|
|
hover_color = "#D32F2F"
|
|
else: # secondary
|
|
bg_color = MaterialColors.SURFACE_VARIANT
|
|
hover_color = MaterialColors.BORDER_LIGHT
|
|
|
|
self.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background-color: {bg_color};
|
|
border: none;
|
|
border-radius: 8px;
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
font-weight: 500;
|
|
padding: 8px 16px;
|
|
min-height: 24px;
|
|
font-size: 13px;
|
|
}}
|
|
QPushButton:hover {{
|
|
background-color: {hover_color};
|
|
}}
|
|
QPushButton:pressed {{
|
|
background-color: {bg_color};
|
|
transform: scale(0.98);
|
|
}}
|
|
QPushButton:disabled {{
|
|
background-color: {MaterialColors.SURFACE_VARIANT};
|
|
color: {MaterialColors.TEXT_DISABLED};
|
|
}}
|
|
""")
|
|
|
|
|
|
class ModernCard(QFrame):
|
|
"""Modern card widget with shadow and rounded corners"""
|
|
|
|
def __init__(self, title="", parent=None):
|
|
super().__init__(parent)
|
|
self.setup_style()
|
|
self.setup_layout(title)
|
|
|
|
def setup_style(self):
|
|
"""Apply card styling"""
|
|
self.setFrameShape(QFrame.Shape.NoFrame)
|
|
self.setStyleSheet(f"""
|
|
QFrame {{
|
|
background-color: {MaterialColors.SURFACE};
|
|
border-radius: 12px;
|
|
border: 1px solid {MaterialColors.BORDER};
|
|
}}
|
|
""")
|
|
|
|
def setup_layout(self, title):
|
|
"""Setup card layout with optional title"""
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(16, 16, 16, 16)
|
|
layout.setSpacing(12)
|
|
|
|
if title:
|
|
title_label = QLabel(title)
|
|
title_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
margin-bottom: 8px;
|
|
}}
|
|
""")
|
|
layout.addWidget(title_label)
|
|
|
|
|
|
class LiveStatsWidget(ModernCard):
|
|
"""Widget for displaying live statistics"""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__("Live Statistics", parent)
|
|
self.setup_stats_ui()
|
|
|
|
# Initialize counters
|
|
self.stats = {
|
|
'vehicles_detected': 0,
|
|
'pedestrians_detected': 0,
|
|
'bicycles_detected': 0,
|
|
'violations_total': 0,
|
|
'violations_today': 0,
|
|
'fps': 0.0
|
|
}
|
|
|
|
# Update timer
|
|
self.update_timer = QTimer()
|
|
self.update_timer.timeout.connect(self.update_display)
|
|
self.update_timer.start(1000) # Update every second
|
|
|
|
def setup_stats_ui(self):
|
|
"""Setup the statistics display"""
|
|
layout = self.layout()
|
|
|
|
# Create stats grid
|
|
stats_grid = QGridLayout()
|
|
|
|
# Vehicle counts
|
|
self.vehicle_label = self.create_stat_widget("Vehicles", "0", MaterialColors.SUCCESS)
|
|
self.pedestrian_label = self.create_stat_widget("Pedestrians", "0", MaterialColors.INFO)
|
|
self.bicycle_label = self.create_stat_widget("Bicycles", "0", MaterialColors.WARNING)
|
|
|
|
# Violation counts
|
|
self.violations_total_label = self.create_stat_widget("Total Violations", "0", MaterialColors.ERROR)
|
|
self.violations_today_label = self.create_stat_widget("Today's Violations", "0", MaterialColors.ERROR)
|
|
|
|
# Performance
|
|
self.fps_label = self.create_stat_widget("FPS", "0.0", MaterialColors.PRIMARY)
|
|
|
|
# Add to grid
|
|
stats_grid.addWidget(self.vehicle_label, 0, 0)
|
|
stats_grid.addWidget(self.pedestrian_label, 0, 1)
|
|
stats_grid.addWidget(self.bicycle_label, 0, 2)
|
|
stats_grid.addWidget(self.violations_total_label, 1, 0)
|
|
stats_grid.addWidget(self.violations_today_label, 1, 1)
|
|
stats_grid.addWidget(self.fps_label, 1, 2)
|
|
|
|
layout.addLayout(stats_grid)
|
|
|
|
def create_stat_widget(self, title, value, color):
|
|
"""Create a single stat display widget"""
|
|
container = QFrame()
|
|
container.setStyleSheet(f"""
|
|
QFrame {{
|
|
background-color: {MaterialColors.BACKGROUND_SECONDARY};
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
margin: 4px;
|
|
}}
|
|
""")
|
|
|
|
layout = QVBoxLayout(container)
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
layout.setSpacing(4)
|
|
|
|
title_label = QLabel(title)
|
|
title_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {MaterialColors.TEXT_SECONDARY};
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
}}
|
|
""")
|
|
|
|
value_label = QLabel(value)
|
|
value_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {color};
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
}}
|
|
""")
|
|
|
|
layout.addWidget(title_label)
|
|
layout.addWidget(value_label)
|
|
|
|
# Store reference to value label for updates
|
|
container.value_label = value_label
|
|
|
|
return container
|
|
|
|
def update_stats(self, new_stats):
|
|
"""Update statistics with new data"""
|
|
self.stats.update(new_stats)
|
|
|
|
def update_display(self):
|
|
"""Update the display with current stats"""
|
|
self.vehicle_label.value_label.setText(str(self.stats['vehicles_detected']))
|
|
self.pedestrian_label.value_label.setText(str(self.stats['pedestrians_detected']))
|
|
self.bicycle_label.value_label.setText(str(self.stats['bicycles_detected']))
|
|
self.violations_total_label.value_label.setText(str(self.stats['violations_total']))
|
|
self.violations_today_label.value_label.setText(str(self.stats['violations_today']))
|
|
self.fps_label.value_label.setText(f"{self.stats['fps']:.1f}")
|
|
|
|
|
|
class ViolationLogWidget(ModernCard):
|
|
"""Advanced violation log table with search and filtering"""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__("Violation Logs", parent)
|
|
self.setup_log_ui()
|
|
self.violations = []
|
|
|
|
def setup_log_ui(self):
|
|
"""Setup the violation log interface"""
|
|
layout = self.layout()
|
|
|
|
# Controls header
|
|
controls_layout = QHBoxLayout()
|
|
|
|
# Search box
|
|
self.search_box = QLineEdit()
|
|
self.search_box.setPlaceholderText("Search violations...")
|
|
self.search_box.setStyleSheet(f"""
|
|
QLineEdit {{
|
|
background-color: {MaterialColors.BACKGROUND_SECONDARY};
|
|
border: 1px solid {MaterialColors.BORDER};
|
|
border-radius: 6px;
|
|
padding: 8px 12px;
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
font-size: 13px;
|
|
}}
|
|
QLineEdit:focus {{
|
|
border-color: {MaterialColors.PRIMARY};
|
|
}}
|
|
""")
|
|
self.search_box.textChanged.connect(self.filter_violations)
|
|
|
|
# Filter dropdown
|
|
self.filter_combo = QComboBox()
|
|
self.filter_combo.addItems(["All Violations", "Red Light", "Crosswalk", "Speed"])
|
|
self.filter_combo.setStyleSheet(f"""
|
|
QComboBox {{
|
|
background-color: {MaterialColors.BACKGROUND_SECONDARY};
|
|
border: 1px solid {MaterialColors.BORDER};
|
|
border-radius: 6px;
|
|
padding: 8px 12px;
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
min-width: 120px;
|
|
}}
|
|
QComboBox::drop-down {{
|
|
border: none;
|
|
}}
|
|
QComboBox::down-arrow {{
|
|
image: none;
|
|
border: none;
|
|
}}
|
|
""")
|
|
self.filter_combo.currentTextChanged.connect(self.filter_violations)
|
|
|
|
# Export button
|
|
self.export_btn = ModernButton("Export Report", button_type="secondary")
|
|
self.export_btn.clicked.connect(self.export_violations)
|
|
|
|
# Clear button
|
|
self.clear_btn = ModernButton("Clear Logs", button_type="error")
|
|
self.clear_btn.clicked.connect(self.clear_violations)
|
|
|
|
controls_layout.addWidget(QLabel("Search:"))
|
|
controls_layout.addWidget(self.search_box)
|
|
controls_layout.addWidget(QLabel("Filter:"))
|
|
controls_layout.addWidget(self.filter_combo)
|
|
controls_layout.addStretch()
|
|
controls_layout.addWidget(self.export_btn)
|
|
controls_layout.addWidget(self.clear_btn)
|
|
|
|
layout.addLayout(controls_layout)
|
|
|
|
# Violation table
|
|
self.violation_table = QTableWidget()
|
|
self.violation_table.setColumnCount(6)
|
|
self.violation_table.setHorizontalHeaderLabels([
|
|
"ID", "Type", "Timestamp", "Object ID", "Confidence", "Actions"
|
|
])
|
|
|
|
# Style the table
|
|
self.violation_table.setStyleSheet(f"""
|
|
QTableWidget {{
|
|
background-color: {MaterialColors.BACKGROUND_SECONDARY};
|
|
border: 1px solid {MaterialColors.BORDER};
|
|
border-radius: 8px;
|
|
gridline-color: {MaterialColors.BORDER};
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
selection-background-color: {MaterialColors.PRIMARY};
|
|
}}
|
|
QTableWidget::item {{
|
|
padding: 8px;
|
|
border-bottom: 1px solid {MaterialColors.BORDER};
|
|
}}
|
|
QTableWidget::item:selected {{
|
|
background-color: {MaterialColors.PRIMARY};
|
|
}}
|
|
QHeaderView::section {{
|
|
background-color: {MaterialColors.SURFACE_VARIANT};
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
padding: 8px;
|
|
border: none;
|
|
font-weight: 600;
|
|
}}
|
|
""")
|
|
|
|
# Configure table
|
|
self.violation_table.horizontalHeader().setStretchLastSection(True)
|
|
self.violation_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
|
self.violation_table.setAlternatingRowColors(True)
|
|
|
|
layout.addWidget(self.violation_table)
|
|
|
|
def add_violation(self, violation_data):
|
|
"""Add a new violation to the log"""
|
|
violation = {
|
|
'id': len(self.violations) + 1,
|
|
'type': violation_data.get('type', 'Unknown'),
|
|
'timestamp': violation_data.get('timestamp', datetime.now()),
|
|
'object_id': violation_data.get('object_id', 'N/A'),
|
|
'confidence': violation_data.get('confidence', 0.0),
|
|
'snapshot_path': violation_data.get('snapshot_path', None)
|
|
}
|
|
|
|
self.violations.append(violation)
|
|
self.update_table()
|
|
|
|
def update_table(self):
|
|
"""Update the violation table display"""
|
|
self.violation_table.setRowCount(len(self.violations))
|
|
|
|
for row, violation in enumerate(self.violations):
|
|
# ID
|
|
self.violation_table.setItem(row, 0, QTableWidgetItem(str(violation['id'])))
|
|
|
|
# Type
|
|
type_item = QTableWidgetItem(violation['type'])
|
|
if violation['type'] == 'Red Light':
|
|
type_item.setForeground(QColor(MaterialColors.ERROR))
|
|
elif violation['type'] == 'Crosswalk':
|
|
type_item.setForeground(QColor(MaterialColors.WARNING))
|
|
self.violation_table.setItem(row, 1, type_item)
|
|
|
|
# Timestamp
|
|
if isinstance(violation['timestamp'], datetime):
|
|
timestamp_str = violation['timestamp'].strftime("%Y-%m-%d %H:%M:%S")
|
|
else:
|
|
timestamp_str = str(violation['timestamp'])
|
|
self.violation_table.setItem(row, 2, QTableWidgetItem(timestamp_str))
|
|
|
|
# Object ID
|
|
self.violation_table.setItem(row, 3, QTableWidgetItem(str(violation['object_id'])))
|
|
|
|
# Confidence
|
|
confidence_str = f"{violation['confidence']:.2f}" if isinstance(violation['confidence'], float) else str(violation['confidence'])
|
|
self.violation_table.setItem(row, 4, QTableWidgetItem(confidence_str))
|
|
|
|
# Actions (View Snapshot button)
|
|
if violation['snapshot_path']:
|
|
view_btn = ModernButton("View", button_type="primary")
|
|
view_btn.clicked.connect(lambda checked, path=violation['snapshot_path']: self.view_snapshot(path))
|
|
self.violation_table.setCellWidget(row, 5, view_btn)
|
|
|
|
def filter_violations(self):
|
|
"""Filter violations based on search and filter criteria"""
|
|
search_text = self.search_box.text().lower()
|
|
filter_type = self.filter_combo.currentText()
|
|
|
|
for row in range(self.violation_table.rowCount()):
|
|
show_row = True
|
|
|
|
# Search filter
|
|
if search_text:
|
|
row_text = ""
|
|
for col in range(self.violation_table.columnCount() - 1): # Exclude actions column
|
|
item = self.violation_table.item(row, col)
|
|
if item:
|
|
row_text += item.text().lower() + " "
|
|
|
|
if search_text not in row_text:
|
|
show_row = False
|
|
|
|
# Type filter
|
|
if filter_type != "All Violations":
|
|
type_item = self.violation_table.item(row, 1)
|
|
if type_item and type_item.text() != filter_type:
|
|
show_row = False
|
|
|
|
self.violation_table.setRowHidden(row, not show_row)
|
|
|
|
def view_snapshot(self, snapshot_path):
|
|
"""View violation snapshot in a popup"""
|
|
dialog = QDialog(self)
|
|
dialog.setWindowTitle("Violation Snapshot")
|
|
dialog.setModal(True)
|
|
dialog.resize(600, 400)
|
|
|
|
layout = QVBoxLayout(dialog)
|
|
|
|
try:
|
|
pixmap = QPixmap(snapshot_path)
|
|
if not pixmap.isNull():
|
|
label = QLabel()
|
|
label.setPixmap(pixmap.scaled(580, 380, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
|
|
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
layout.addWidget(label)
|
|
else:
|
|
layout.addWidget(QLabel("Error: Could not load snapshot"))
|
|
except Exception as e:
|
|
layout.addWidget(QLabel(f"Error loading snapshot: {str(e)}"))
|
|
|
|
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
|
|
buttons.accepted.connect(dialog.accept)
|
|
layout.addWidget(buttons)
|
|
|
|
dialog.exec()
|
|
|
|
def export_violations(self):
|
|
"""Export violations to CSV file"""
|
|
if not self.violations:
|
|
QMessageBox.information(self, "Export", "No violations to export.")
|
|
return
|
|
|
|
file_path, _ = QFileDialog.getSaveFileName(
|
|
self, "Export Violations", "violations_report.csv", "CSV Files (*.csv)"
|
|
)
|
|
|
|
if file_path:
|
|
try:
|
|
import csv
|
|
with open(file_path, 'w', newline='', encoding='utf-8') as csvfile:
|
|
writer = csv.writer(csvfile)
|
|
writer.writerow(['ID', 'Type', 'Timestamp', 'Object ID', 'Confidence', 'Snapshot Path'])
|
|
|
|
for violation in self.violations:
|
|
timestamp_str = violation['timestamp'].strftime("%Y-%m-%d %H:%M:%S") if isinstance(violation['timestamp'], datetime) else str(violation['timestamp'])
|
|
writer.writerow([
|
|
violation['id'],
|
|
violation['type'],
|
|
timestamp_str,
|
|
violation['object_id'],
|
|
violation['confidence'],
|
|
violation.get('snapshot_path', '')
|
|
])
|
|
|
|
QMessageBox.information(self, "Export", f"Violations exported to {file_path}")
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Export Error", f"Failed to export violations:\n{str(e)}")
|
|
|
|
def clear_violations(self):
|
|
"""Clear all violation logs"""
|
|
reply = QMessageBox.question(
|
|
self, "Clear Logs", "Are you sure you want to clear all violation logs?",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
QMessageBox.StandardButton.No
|
|
)
|
|
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
self.violations.clear()
|
|
self.violation_table.setRowCount(0)
|
|
|
|
|
|
class VideoControlsWidget(QFrame):
|
|
"""Modern video control toolbar"""
|
|
|
|
# Signals
|
|
load_video_requested = Signal()
|
|
play_requested = Signal()
|
|
pause_requested = Signal()
|
|
stop_requested = Signal()
|
|
snapshot_requested = Signal()
|
|
fullscreen_requested = Signal()
|
|
position_changed = Signal(int)
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setup_controls()
|
|
self.is_playing = False
|
|
self.video_duration = 0
|
|
|
|
def setup_controls(self):
|
|
"""Setup video control interface"""
|
|
self.setStyleSheet(f"""
|
|
QFrame {{
|
|
background-color: {MaterialColors.SURFACE};
|
|
border-top: 1px solid {MaterialColors.BORDER};
|
|
padding: 8px;
|
|
}}
|
|
""")
|
|
|
|
layout = QHBoxLayout(self)
|
|
layout.setContentsMargins(16, 8, 16, 8)
|
|
layout.setSpacing(12)
|
|
|
|
# Load video button
|
|
self.load_btn = ModernButton("📂 Load Video", button_type="secondary")
|
|
self.load_btn.clicked.connect(self.load_video_requested.emit)
|
|
layout.addWidget(self.load_btn)
|
|
|
|
layout.addWidget(self.create_separator())
|
|
|
|
# Playback controls
|
|
self.play_btn = ModernButton("▶️ Play", button_type="success")
|
|
self.play_btn.clicked.connect(self.toggle_playback)
|
|
layout.addWidget(self.play_btn)
|
|
|
|
self.stop_btn = ModernButton("⏹️ Stop", button_type="error")
|
|
self.stop_btn.clicked.connect(self.stop_video)
|
|
layout.addWidget(self.stop_btn)
|
|
|
|
layout.addWidget(self.create_separator())
|
|
|
|
# Progress slider
|
|
self.progress_slider = QSlider(Qt.Orientation.Horizontal)
|
|
self.progress_slider.setMinimum(0)
|
|
self.progress_slider.setMaximum(100)
|
|
self.progress_slider.setValue(0)
|
|
self.progress_slider.setStyleSheet(f"""
|
|
QSlider::groove:horizontal {{
|
|
background: {MaterialColors.BACKGROUND_SECONDARY};
|
|
height: 6px;
|
|
border-radius: 3px;
|
|
}}
|
|
QSlider::handle:horizontal {{
|
|
background: {MaterialColors.PRIMARY};
|
|
width: 16px;
|
|
height: 16px;
|
|
border-radius: 8px;
|
|
margin: -5px 0;
|
|
}}
|
|
QSlider::sub-page:horizontal {{
|
|
background: {MaterialColors.PRIMARY};
|
|
border-radius: 3px;
|
|
}}
|
|
""")
|
|
self.progress_slider.sliderPressed.connect(self.on_slider_pressed)
|
|
self.progress_slider.sliderReleased.connect(self.on_slider_released)
|
|
layout.addWidget(self.progress_slider, 1)
|
|
|
|
# Time display
|
|
self.time_label = QLabel("00:00 / 00:00")
|
|
self.time_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {MaterialColors.TEXT_SECONDARY};
|
|
font-family: 'Consolas', monospace;
|
|
font-size: 12px;
|
|
min-width: 80px;
|
|
}}
|
|
""")
|
|
layout.addWidget(self.time_label)
|
|
|
|
layout.addWidget(self.create_separator())
|
|
|
|
# Additional controls
|
|
self.snapshot_btn = ModernButton("📸 Snapshot", button_type="secondary")
|
|
self.snapshot_btn.clicked.connect(self.snapshot_requested.emit)
|
|
layout.addWidget(self.snapshot_btn)
|
|
|
|
self.fullscreen_btn = ModernButton("⛶ Fullscreen", button_type="secondary")
|
|
self.fullscreen_btn.clicked.connect(self.fullscreen_requested.emit)
|
|
layout.addWidget(self.fullscreen_btn)
|
|
|
|
def create_separator(self):
|
|
"""Create a visual separator"""
|
|
separator = QFrame()
|
|
separator.setFrameShape(QFrame.Shape.VLine)
|
|
separator.setFrameShadow(QFrame.Shadow.Sunken)
|
|
separator.setStyleSheet(f"""
|
|
QFrame {{
|
|
color: {MaterialColors.BORDER};
|
|
max-width: 1px;
|
|
}}
|
|
""")
|
|
return separator
|
|
|
|
def toggle_playback(self):
|
|
"""Toggle between play and pause"""
|
|
if self.is_playing:
|
|
self.pause_video()
|
|
else:
|
|
self.play_video()
|
|
|
|
def play_video(self):
|
|
"""Start video playback"""
|
|
self.is_playing = True
|
|
self.play_btn.setText("⏸️ Pause")
|
|
self.play_requested.emit()
|
|
|
|
def pause_video(self):
|
|
"""Pause video playback"""
|
|
self.is_playing = False
|
|
self.play_btn.setText("▶️ Play")
|
|
self.pause_requested.emit()
|
|
|
|
def stop_video(self):
|
|
"""Stop video playback"""
|
|
self.is_playing = False
|
|
self.play_btn.setText("▶️ Play")
|
|
self.progress_slider.setValue(0)
|
|
self.update_time_display(0, self.video_duration)
|
|
self.stop_requested.emit()
|
|
|
|
def update_progress(self, position, duration):
|
|
"""Update progress slider and time display"""
|
|
self.video_duration = duration
|
|
if duration > 0:
|
|
progress = int((position / duration) * 100)
|
|
self.progress_slider.setValue(progress)
|
|
self.update_time_display(position, duration)
|
|
|
|
def update_time_display(self, position, duration):
|
|
"""Update time display label"""
|
|
pos_time = self.format_time(position)
|
|
dur_time = self.format_time(duration)
|
|
self.time_label.setText(f"{pos_time} / {dur_time}")
|
|
|
|
def format_time(self, seconds):
|
|
"""Format time in MM:SS format"""
|
|
minutes = int(seconds // 60)
|
|
seconds = int(seconds % 60)
|
|
return f"{minutes:02d}:{seconds:02d}"
|
|
|
|
def on_slider_pressed(self):
|
|
"""Handle slider press - pause during seeking"""
|
|
self.seeking = True
|
|
|
|
def on_slider_released(self):
|
|
"""Handle slider release - emit position change"""
|
|
self.seeking = False
|
|
position = (self.progress_slider.value() / 100.0) * self.video_duration
|
|
self.position_changed.emit(int(position))
|
|
|
|
|
|
class DetectionControlsWidget(ModernCard):
|
|
"""Controls for detection and tracking settings"""
|
|
|
|
# Signals
|
|
detection_toggled = Signal(bool)
|
|
tracking_toggled = Signal(bool)
|
|
confidence_changed = Signal(float)
|
|
class_visibility_changed = Signal(str, bool)
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__("Detection Controls", parent)
|
|
self.setup_controls()
|
|
|
|
def setup_controls(self):
|
|
"""Setup detection control interface"""
|
|
layout = self.layout()
|
|
|
|
# Main toggle switches
|
|
toggles_layout = QHBoxLayout()
|
|
|
|
self.detection_checkbox = QCheckBox("Enable Detection")
|
|
self.detection_checkbox.setChecked(True)
|
|
self.detection_checkbox.toggled.connect(self.detection_toggled.emit)
|
|
self.detection_checkbox.setStyleSheet(f"""
|
|
QCheckBox {{
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
}}
|
|
QCheckBox::indicator {{
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 3px;
|
|
border: 2px solid {MaterialColors.BORDER};
|
|
}}
|
|
QCheckBox::indicator:checked {{
|
|
background-color: {MaterialColors.SUCCESS};
|
|
border-color: {MaterialColors.SUCCESS};
|
|
}}
|
|
""")
|
|
|
|
self.tracking_checkbox = QCheckBox("Enable Tracking")
|
|
self.tracking_checkbox.setChecked(True)
|
|
self.tracking_checkbox.toggled.connect(self.tracking_toggled.emit)
|
|
self.tracking_checkbox.setStyleSheet(self.detection_checkbox.styleSheet())
|
|
|
|
toggles_layout.addWidget(self.detection_checkbox)
|
|
toggles_layout.addWidget(self.tracking_checkbox)
|
|
toggles_layout.addStretch()
|
|
|
|
layout.addLayout(toggles_layout)
|
|
|
|
# Confidence threshold
|
|
conf_layout = QHBoxLayout()
|
|
conf_layout.addWidget(QLabel("Confidence Threshold:"))
|
|
|
|
self.confidence_slider = QSlider(Qt.Orientation.Horizontal)
|
|
self.confidence_slider.setMinimum(1)
|
|
self.confidence_slider.setMaximum(100)
|
|
self.confidence_slider.setValue(50)
|
|
self.confidence_slider.setStyleSheet(f"""
|
|
QSlider::groove:horizontal {{
|
|
background: {MaterialColors.BACKGROUND_SECONDARY};
|
|
height: 4px;
|
|
border-radius: 2px;
|
|
}}
|
|
QSlider::handle:horizontal {{
|
|
background: {MaterialColors.PRIMARY};
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 7px;
|
|
margin: -5px 0;
|
|
}}
|
|
QSlider::sub-page:horizontal {{
|
|
background: {MaterialColors.PRIMARY};
|
|
border-radius: 2px;
|
|
}}
|
|
""")
|
|
self.confidence_slider.valueChanged.connect(self.on_confidence_changed)
|
|
|
|
self.confidence_label = QLabel("0.50")
|
|
self.confidence_label.setMinimumWidth(40)
|
|
self.confidence_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
font-family: 'Consolas', monospace;
|
|
font-size: 12px;
|
|
}}
|
|
""")
|
|
|
|
conf_layout.addWidget(self.confidence_slider)
|
|
conf_layout.addWidget(self.confidence_label)
|
|
|
|
layout.addLayout(conf_layout)
|
|
|
|
# Class visibility toggles
|
|
class_layout = QVBoxLayout()
|
|
class_layout.addWidget(QLabel("Object Classes:"))
|
|
|
|
class_grid = QGridLayout()
|
|
|
|
self.class_checkboxes = {}
|
|
classes = [
|
|
("Vehicles", MaterialColors.SUCCESS),
|
|
("Pedestrians", MaterialColors.INFO),
|
|
("Bicycles", MaterialColors.WARNING)
|
|
]
|
|
|
|
for i, (class_name, color) in enumerate(classes):
|
|
checkbox = QCheckBox(class_name)
|
|
checkbox.setChecked(True)
|
|
checkbox.setStyleSheet(f"""
|
|
QCheckBox {{
|
|
color: {color};
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
}}
|
|
QCheckBox::indicator {{
|
|
width: 16px;
|
|
height: 16px;
|
|
border-radius: 3px;
|
|
border: 2px solid {color};
|
|
}}
|
|
QCheckBox::indicator:checked {{
|
|
background-color: {color};
|
|
}}
|
|
""")
|
|
checkbox.toggled.connect(lambda checked, name=class_name: self.class_visibility_changed.emit(name, checked))
|
|
|
|
self.class_checkboxes[class_name] = checkbox
|
|
class_grid.addWidget(checkbox, i // 2, i % 2)
|
|
|
|
class_layout.addLayout(class_grid)
|
|
layout.addLayout(class_layout)
|
|
|
|
def on_confidence_changed(self, value):
|
|
"""Handle confidence slider change"""
|
|
confidence = value / 100.0
|
|
self.confidence_label.setText(f"{confidence:.2f}")
|
|
self.confidence_changed.emit(confidence)
|
|
|
|
|
|
class AnalyticsWidget(QWidget):
|
|
"""Analytics dashboard with charts and statistics"""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setup_analytics()
|
|
|
|
# Initialize data
|
|
self.traffic_data = []
|
|
self.violation_data = []
|
|
|
|
# Update timer
|
|
self.update_timer = QTimer()
|
|
self.update_timer.timeout.connect(self.update_charts)
|
|
self.update_timer.start(5000) # Update every 5 seconds
|
|
|
|
def setup_analytics(self):
|
|
"""Setup analytics dashboard"""
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(16, 16, 16, 16)
|
|
layout.setSpacing(16)
|
|
|
|
# Title
|
|
title = QLabel("Analytics Dashboard")
|
|
title.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
margin-bottom: 16px;
|
|
}}
|
|
""")
|
|
layout.addWidget(title)
|
|
|
|
if PYQTGRAPH_AVAILABLE:
|
|
self.setup_charts(layout)
|
|
else:
|
|
# Fallback when PyQtGraph is not available
|
|
fallback_label = QLabel("PyQtGraph not available. Charts disabled.")
|
|
fallback_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {MaterialColors.TEXT_SECONDARY};
|
|
font-size: 14px;
|
|
text-align: center;
|
|
padding: 40px;
|
|
background-color: {MaterialColors.SURFACE};
|
|
border-radius: 8px;
|
|
}}
|
|
""")
|
|
layout.addWidget(fallback_label)
|
|
|
|
def setup_charts(self, layout):
|
|
"""Setup chart widgets"""
|
|
# Configure PyQtGraph
|
|
pg.setConfigOption('background', MaterialColors.BACKGROUND_SECONDARY)
|
|
pg.setConfigOption('foreground', MaterialColors.TEXT_PRIMARY)
|
|
|
|
# Charts container
|
|
charts_splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
|
|
# Traffic flow chart
|
|
traffic_widget = pg.PlotWidget(title="Traffic Flow (Objects/Minute)")
|
|
traffic_widget.setLabel('left', 'Count')
|
|
traffic_widget.setLabel('bottom', 'Time (minutes)')
|
|
traffic_widget.showGrid(x=True, y=True, alpha=0.3)
|
|
|
|
self.traffic_curve = traffic_widget.plot(
|
|
pen=pg.mkPen(color=MaterialColors.PRIMARY, width=2),
|
|
name="Traffic Flow"
|
|
)
|
|
|
|
charts_splitter.addWidget(traffic_widget)
|
|
|
|
# Violations chart
|
|
violations_widget = pg.PlotWidget(title="Violations Over Time")
|
|
violations_widget.setLabel('left', 'Violations')
|
|
violations_widget.setLabel('bottom', 'Time (minutes)')
|
|
violations_widget.showGrid(x=True, y=True, alpha=0.3)
|
|
|
|
self.violations_curve = violations_widget.plot(
|
|
pen=pg.mkPen(color=MaterialColors.ERROR, width=2),
|
|
name="Violations"
|
|
)
|
|
|
|
charts_splitter.addWidget(violations_widget)
|
|
|
|
layout.addWidget(charts_splitter)
|
|
|
|
def update_charts(self):
|
|
"""Update chart data"""
|
|
if not PYQTGRAPH_AVAILABLE:
|
|
return
|
|
|
|
# Simulate or get real data
|
|
import time
|
|
current_time = time.time()
|
|
|
|
# Update traffic data (you would replace this with real data)
|
|
if len(self.traffic_data) > 60: # Keep last 60 points
|
|
self.traffic_data.pop(0)
|
|
|
|
# Add new data point (replace with real traffic count)
|
|
self.traffic_data.append((current_time, len(self.traffic_data) % 20 + 10))
|
|
|
|
# Update violation data
|
|
if len(self.violation_data) > 60:
|
|
self.violation_data.pop(0)
|
|
|
|
# Add new data point (replace with real violation count)
|
|
self.violation_data.append((current_time, len(self.violation_data) % 5))
|
|
|
|
# Update curves
|
|
if self.traffic_data:
|
|
x_traffic = [point[0] - self.traffic_data[0][0] for point in self.traffic_data]
|
|
y_traffic = [point[1] for point in self.traffic_data]
|
|
self.traffic_curve.setData(x_traffic, y_traffic)
|
|
|
|
if self.violation_data:
|
|
x_violations = [point[0] - self.violation_data[0][0] for point in self.violation_data]
|
|
y_violations = [point[1] for point in self.violation_data]
|
|
self.violations_curve.setData(x_violations, y_violations)
|
|
|
|
|
|
class TrafficMonitoringUI(QMainWindow):
|
|
"""Main application window with advanced UI design"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.setup_ui()
|
|
self.setup_styling()
|
|
self.setup_shortcuts()
|
|
|
|
# Initialize components
|
|
self.video_loaded = False
|
|
self.detection_active = False
|
|
|
|
def setup_ui(self):
|
|
"""Setup the main user interface"""
|
|
self.setWindowTitle("Traffic Intersection Monitoring System")
|
|
self.setMinimumSize(1200, 800)
|
|
self.resize(1400, 900)
|
|
|
|
# Set application icon (if available)
|
|
# self.setWindowIcon(QIcon("path/to/icon.png"))
|
|
|
|
# Central widget with tab layout
|
|
central_widget = QWidget()
|
|
self.setCentralWidget(central_widget)
|
|
|
|
layout = QVBoxLayout(central_widget)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(0)
|
|
|
|
# Create tab widget
|
|
self.tab_widget = QTabWidget()
|
|
self.tab_widget.setStyleSheet(f"""
|
|
QTabWidget::pane {{
|
|
border: none;
|
|
background-color: {MaterialColors.BACKGROUND_PRIMARY};
|
|
}}
|
|
QTabBar::tab {{
|
|
background-color: {MaterialColors.SURFACE};
|
|
color: {MaterialColors.TEXT_SECONDARY};
|
|
padding: 12px 24px;
|
|
margin-right: 2px;
|
|
border-top-left-radius: 8px;
|
|
border-top-right-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
}}
|
|
QTabBar::tab:selected {{
|
|
background-color: {MaterialColors.BACKGROUND_PRIMARY};
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
border-bottom: 2px solid {MaterialColors.PRIMARY};
|
|
}}
|
|
QTabBar::tab:hover {{
|
|
background-color: {MaterialColors.SURFACE_VARIANT};
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
}}
|
|
""")
|
|
|
|
# Create tabs
|
|
self.create_live_monitoring_tab()
|
|
self.create_detection_tab()
|
|
self.create_violations_tab()
|
|
self.create_analytics_tab()
|
|
|
|
layout.addWidget(self.tab_widget)
|
|
|
|
# Create status bar
|
|
self.setup_status_bar()
|
|
|
|
# Create menu bar
|
|
self.setup_menu_bar()
|
|
|
|
def create_live_monitoring_tab(self):
|
|
"""Create the live monitoring tab"""
|
|
tab = QWidget()
|
|
layout = QVBoxLayout(tab)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(0)
|
|
|
|
# Main content area
|
|
content_splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
|
|
# Video display area
|
|
video_frame = QFrame()
|
|
video_frame.setStyleSheet(f"""
|
|
QFrame {{
|
|
background-color: {MaterialColors.BACKGROUND_SECONDARY};
|
|
border: 2px solid {MaterialColors.BORDER};
|
|
border-radius: 8px;
|
|
}}
|
|
""")
|
|
video_layout = QVBoxLayout(video_frame)
|
|
|
|
# Video placeholder
|
|
self.video_label = QLabel("Load a video to start monitoring")
|
|
self.video_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
self.video_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {MaterialColors.TEXT_SECONDARY};
|
|
font-size: 18px;
|
|
padding: 40px;
|
|
}}
|
|
""")
|
|
video_layout.addWidget(self.video_label)
|
|
|
|
content_splitter.addWidget(video_frame)
|
|
|
|
# Side panel with stats
|
|
side_panel = QWidget()
|
|
side_panel.setMaximumWidth(350)
|
|
side_layout = QVBoxLayout(side_panel)
|
|
side_layout.setContentsMargins(16, 16, 16, 16)
|
|
side_layout.setSpacing(16)
|
|
|
|
# Live stats
|
|
self.live_stats = LiveStatsWidget()
|
|
side_layout.addWidget(self.live_stats)
|
|
|
|
# Detection controls
|
|
self.detection_controls = DetectionControlsWidget()
|
|
side_layout.addWidget(self.detection_controls)
|
|
|
|
side_layout.addStretch()
|
|
|
|
content_splitter.addWidget(side_panel)
|
|
content_splitter.setSizes([800, 350])
|
|
|
|
layout.addWidget(content_splitter)
|
|
|
|
# Video controls at bottom
|
|
self.video_controls = VideoControlsWidget()
|
|
layout.addWidget(self.video_controls)
|
|
|
|
self.tab_widget.addTab(tab, "🎥 Live Monitoring")
|
|
|
|
def create_detection_tab(self):
|
|
"""Create the detection visualization tab"""
|
|
tab = QWidget()
|
|
layout = QVBoxLayout(tab)
|
|
layout.setContentsMargins(16, 16, 16, 16)
|
|
layout.setSpacing(16)
|
|
|
|
# Title
|
|
title = QLabel("Detection & Tracking Visualization")
|
|
title.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
margin-bottom: 8px;
|
|
}}
|
|
""")
|
|
layout.addWidget(title)
|
|
|
|
# Detection display area
|
|
detection_frame = QFrame()
|
|
detection_frame.setStyleSheet(f"""
|
|
QFrame {{
|
|
background-color: {MaterialColors.BACKGROUND_SECONDARY};
|
|
border: 1px solid {MaterialColors.BORDER};
|
|
border-radius: 8px;
|
|
min-height: 400px;
|
|
}}
|
|
""")
|
|
|
|
detection_layout = QVBoxLayout(detection_frame)
|
|
|
|
# Detection placeholder
|
|
detection_label = QLabel("Detection visualization will appear here")
|
|
detection_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
detection_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {MaterialColors.TEXT_SECONDARY};
|
|
font-size: 16px;
|
|
padding: 40px;
|
|
}}
|
|
""")
|
|
detection_layout.addWidget(detection_label)
|
|
|
|
layout.addWidget(detection_frame, 1)
|
|
|
|
# Detection legend
|
|
legend_frame = ModernCard("Detection Legend")
|
|
legend_layout = QHBoxLayout()
|
|
|
|
legend_items = [
|
|
("🚗 Vehicles", MaterialColors.SUCCESS),
|
|
("🚶 Pedestrians", MaterialColors.INFO),
|
|
("🚴 Bicycles", MaterialColors.WARNING),
|
|
("🚨 Violations", MaterialColors.ERROR)
|
|
]
|
|
|
|
for text, color in legend_items:
|
|
legend_label = QLabel(text)
|
|
legend_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {color};
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
padding: 8px 16px;
|
|
background-color: {MaterialColors.BACKGROUND_SECONDARY};
|
|
border-radius: 6px;
|
|
margin: 4px;
|
|
}}
|
|
""")
|
|
legend_layout.addWidget(legend_label)
|
|
|
|
legend_layout.addStretch()
|
|
legend_frame.layout().addLayout(legend_layout)
|
|
layout.addWidget(legend_frame)
|
|
|
|
self.tab_widget.addTab(tab, "🎯 Detection")
|
|
|
|
def create_violations_tab(self):
|
|
"""Create the violations and statistics tab"""
|
|
tab = QWidget()
|
|
layout = QVBoxLayout(tab)
|
|
layout.setContentsMargins(16, 16, 16, 16)
|
|
layout.setSpacing(16)
|
|
|
|
# Title
|
|
title = QLabel("Violations & Reports")
|
|
title.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
margin-bottom: 8px;
|
|
}}
|
|
""")
|
|
layout.addWidget(title)
|
|
|
|
# Violation log widget
|
|
self.violation_log = ViolationLogWidget()
|
|
layout.addWidget(self.violation_log, 1)
|
|
|
|
self.tab_widget.addTab(tab, "🚨 Violations")
|
|
|
|
def create_analytics_tab(self):
|
|
"""Create the analytics dashboard tab"""
|
|
self.analytics_widget = AnalyticsWidget()
|
|
self.tab_widget.addTab(self.analytics_widget, "📊 Analytics")
|
|
|
|
def setup_styling(self):
|
|
"""Apply global styling to the application"""
|
|
self.setStyleSheet(f"""
|
|
QMainWindow {{
|
|
background-color: {MaterialColors.BACKGROUND_PRIMARY};
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
}}
|
|
QLabel {{
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
}}
|
|
QWidget {{
|
|
background-color: {MaterialColors.BACKGROUND_PRIMARY};
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
}}
|
|
""")
|
|
|
|
# Set application font
|
|
font = QFont("Segoe UI", 10)
|
|
font.setHintingPreference(QFont.HintingPreference.PreferDefaultHinting)
|
|
self.setFont(font)
|
|
QApplication.instance().setFont(font)
|
|
|
|
def setup_menu_bar(self):
|
|
"""Setup the application menu bar"""
|
|
menubar = self.menuBar()
|
|
menubar.setStyleSheet(f"""
|
|
QMenuBar {{
|
|
background-color: {MaterialColors.SURFACE};
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
border-bottom: 1px solid {MaterialColors.BORDER};
|
|
padding: 4px;
|
|
}}
|
|
QMenuBar::item {{
|
|
background: transparent;
|
|
padding: 6px 12px;
|
|
border-radius: 4px;
|
|
}}
|
|
QMenuBar::item:selected {{
|
|
background-color: {MaterialColors.SURFACE_VARIANT};
|
|
}}
|
|
QMenu {{
|
|
background-color: {MaterialColors.SURFACE};
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
border: 1px solid {MaterialColors.BORDER};
|
|
border-radius: 6px;
|
|
padding: 4px;
|
|
}}
|
|
QMenu::item {{
|
|
padding: 6px 16px;
|
|
border-radius: 4px;
|
|
}}
|
|
QMenu::item:selected {{
|
|
background-color: {MaterialColors.PRIMARY};
|
|
}}
|
|
""")
|
|
|
|
# File menu
|
|
file_menu = menubar.addMenu("File")
|
|
|
|
load_action = QAction("Load Video...", self)
|
|
load_action.setShortcut(QKeySequence.StandardKey.Open)
|
|
load_action.triggered.connect(self.load_video)
|
|
file_menu.addAction(load_action)
|
|
|
|
file_menu.addSeparator()
|
|
|
|
export_action = QAction("Export Report...", self)
|
|
export_action.setShortcut("Ctrl+E")
|
|
export_action.triggered.connect(self.export_report)
|
|
file_menu.addAction(export_action)
|
|
|
|
file_menu.addSeparator()
|
|
|
|
exit_action = QAction("Exit", self)
|
|
exit_action.setShortcut(QKeySequence.StandardKey.Quit)
|
|
exit_action.triggered.connect(self.close)
|
|
file_menu.addAction(exit_action)
|
|
|
|
# View menu
|
|
view_menu = menubar.addMenu("View")
|
|
|
|
fullscreen_action = QAction("Fullscreen", self)
|
|
fullscreen_action.setShortcut("F11")
|
|
fullscreen_action.triggered.connect(self.toggle_fullscreen)
|
|
view_menu.addAction(fullscreen_action)
|
|
|
|
# Help menu
|
|
help_menu = menubar.addMenu("Help")
|
|
|
|
about_action = QAction("About", self)
|
|
about_action.triggered.connect(self.show_about)
|
|
help_menu.addAction(about_action)
|
|
|
|
def setup_status_bar(self):
|
|
"""Setup the status bar"""
|
|
status_bar = self.statusBar()
|
|
status_bar.setStyleSheet(f"""
|
|
QStatusBar {{
|
|
background-color: {MaterialColors.SURFACE};
|
|
color: {MaterialColors.TEXT_SECONDARY};
|
|
border-top: 1px solid {MaterialColors.BORDER};
|
|
font-size: 12px;
|
|
}}
|
|
""")
|
|
|
|
status_bar.showMessage("Ready - Load a video to start monitoring")
|
|
|
|
def setup_shortcuts(self):
|
|
"""Setup keyboard shortcuts"""
|
|
# Space for play/pause
|
|
play_shortcut = QAction(self)
|
|
play_shortcut.setShortcut("Space")
|
|
play_shortcut.triggered.connect(self.video_controls.toggle_playback)
|
|
self.addAction(play_shortcut)
|
|
|
|
# S for snapshot
|
|
snapshot_shortcut = QAction(self)
|
|
snapshot_shortcut.setShortcut("S")
|
|
snapshot_shortcut.triggered.connect(self.video_controls.snapshot_requested.emit)
|
|
self.addAction(snapshot_shortcut)
|
|
|
|
def load_video(self):
|
|
"""Load video file"""
|
|
file_path, _ = QFileDialog.getOpenFileName(
|
|
self, "Load Video", "",
|
|
"Video Files (*.mp4 *.avi *.mov *.mkv *.flv *.wmv);;All Files (*)"
|
|
)
|
|
|
|
if file_path:
|
|
self.video_loaded = True
|
|
self.video_label.setText(f"Video loaded: {file_path.split('/')[-1]}")
|
|
self.statusBar().showMessage(f"Video loaded: {file_path}")
|
|
|
|
# Enable controls
|
|
self.video_controls.load_btn.setText("📂 Change Video")
|
|
|
|
def export_report(self):
|
|
"""Export monitoring report"""
|
|
self.violation_log.export_violations()
|
|
|
|
def toggle_fullscreen(self):
|
|
"""Toggle fullscreen mode"""
|
|
if self.isFullScreen():
|
|
self.showNormal()
|
|
else:
|
|
self.showFullScreen()
|
|
|
|
def show_about(self):
|
|
"""Show about dialog"""
|
|
QMessageBox.about(
|
|
self, "About",
|
|
"Traffic Intersection Monitoring System\n\n"
|
|
"An advanced AI-powered system for monitoring traffic\n"
|
|
"intersections and detecting violations.\n\n"
|
|
"Features:\n"
|
|
"• Real-time object detection and tracking\n"
|
|
"• Violation detection and logging\n"
|
|
"• Advanced analytics and reporting\n"
|
|
"• Modern dark theme UI\n\n"
|
|
"Built with PySide6, OpenCV, and YOLO"
|
|
)
|
|
|
|
def update_stats(self, stats_data):
|
|
"""Update live statistics"""
|
|
self.live_stats.update_stats(stats_data)
|
|
|
|
def add_violation(self, violation_data):
|
|
"""Add a new violation to the log"""
|
|
self.violation_log.add_violation(violation_data)
|
|
|
|
# Update status bar
|
|
self.statusBar().showMessage(
|
|
f"New violation detected: {violation_data.get('type', 'Unknown')}"
|
|
)
|
|
|
|
|
|
class SplashScreen(QWidget):
|
|
"""Modern splash screen for application startup"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.setup_splash()
|
|
|
|
# Auto-close timer
|
|
self.timer = QTimer()
|
|
self.timer.timeout.connect(self.close)
|
|
self.timer.start(3000) # Show for 3 seconds
|
|
|
|
def setup_splash(self):
|
|
"""Setup splash screen UI"""
|
|
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint)
|
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
|
self.setFixedSize(400, 300)
|
|
|
|
# Center on screen
|
|
screen = QApplication.primaryScreen().geometry()
|
|
self.move(
|
|
(screen.width() - self.width()) // 2,
|
|
(screen.height() - self.height()) // 2
|
|
)
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(40, 40, 40, 40)
|
|
layout.setSpacing(20)
|
|
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
# Main container
|
|
container = QFrame()
|
|
container.setStyleSheet(f"""
|
|
QFrame {{
|
|
background-color: {MaterialColors.SURFACE};
|
|
border-radius: 16px;
|
|
border: 1px solid {MaterialColors.BORDER};
|
|
}}
|
|
""")
|
|
container_layout = QVBoxLayout(container)
|
|
container_layout.setContentsMargins(40, 40, 40, 40)
|
|
container_layout.setSpacing(20)
|
|
container_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
# Logo/Icon placeholder
|
|
logo_label = QLabel("🚦")
|
|
logo_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
font-size: 48px;
|
|
color: {MaterialColors.PRIMARY};
|
|
margin-bottom: 10px;
|
|
}}
|
|
""")
|
|
logo_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
container_layout.addWidget(logo_label)
|
|
|
|
# Title
|
|
title_label = QLabel("Traffic Monitoring System")
|
|
title_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {MaterialColors.TEXT_PRIMARY};
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
margin-bottom: 5px;
|
|
}}
|
|
""")
|
|
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
container_layout.addWidget(title_label)
|
|
|
|
# Subtitle
|
|
subtitle_label = QLabel("Loading AI-Powered Monitoring...")
|
|
subtitle_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
color: {MaterialColors.TEXT_SECONDARY};
|
|
font-size: 14px;
|
|
margin-bottom: 20px;
|
|
}}
|
|
""")
|
|
subtitle_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
container_layout.addWidget(subtitle_label)
|
|
|
|
# Progress bar
|
|
self.progress_bar = QProgressBar()
|
|
self.progress_bar.setRange(0, 0) # Indeterminate progress
|
|
self.progress_bar.setStyleSheet(f"""
|
|
QProgressBar {{
|
|
border: none;
|
|
border-radius: 6px;
|
|
background-color: {MaterialColors.BACKGROUND_SECONDARY};
|
|
height: 12px;
|
|
}}
|
|
QProgressBar::chunk {{
|
|
background-color: {MaterialColors.PRIMARY};
|
|
border-radius: 6px;
|
|
}}
|
|
""")
|
|
container_layout.addWidget(self.progress_bar)
|
|
|
|
layout.addWidget(container)
|
|
|
|
|
|
def main():
|
|
"""Main application entry point"""
|
|
app = QApplication(sys.argv)
|
|
app.setApplicationName("Traffic Monitoring System")
|
|
app.setApplicationVersion("1.0")
|
|
app.setOrganizationName("Traffic AI Solutions")
|
|
|
|
# Set application style
|
|
app.setStyle("Fusion")
|
|
|
|
# Apply dark palette
|
|
palette = QPalette()
|
|
palette.setColor(QPalette.ColorRole.Window, QColor(MaterialColors.BACKGROUND_PRIMARY))
|
|
palette.setColor(QPalette.ColorRole.WindowText, QColor(MaterialColors.TEXT_PRIMARY))
|
|
palette.setColor(QPalette.ColorRole.Base, QColor(MaterialColors.BACKGROUND_SECONDARY))
|
|
palette.setColor(QPalette.ColorRole.AlternateBase, QColor(MaterialColors.SURFACE_VARIANT))
|
|
palette.setColor(QPalette.ColorRole.ToolTipBase, QColor(MaterialColors.SURFACE))
|
|
palette.setColor(QPalette.ColorRole.ToolTipText, QColor(MaterialColors.TEXT_PRIMARY))
|
|
palette.setColor(QPalette.ColorRole.Text, QColor(MaterialColors.TEXT_PRIMARY))
|
|
palette.setColor(QPalette.ColorRole.Button, QColor(MaterialColors.SURFACE))
|
|
palette.setColor(QPalette.ColorRole.ButtonText, QColor(MaterialColors.TEXT_PRIMARY))
|
|
palette.setColor(QPalette.ColorRole.BrightText, QColor(MaterialColors.ERROR))
|
|
palette.setColor(QPalette.ColorRole.Link, QColor(MaterialColors.PRIMARY))
|
|
palette.setColor(QPalette.ColorRole.Highlight, QColor(MaterialColors.PRIMARY))
|
|
palette.setColor(QPalette.ColorRole.HighlightedText, QColor(MaterialColors.TEXT_PRIMARY))
|
|
app.setPalette(palette)
|
|
|
|
# Show splash screen
|
|
splash = SplashScreen()
|
|
splash.show()
|
|
|
|
# Process events to show splash
|
|
app.processEvents()
|
|
|
|
# Create and show main window
|
|
window = TrafficMonitoringUI()
|
|
|
|
# Close splash and show main window
|
|
splash.close()
|
|
window.show()
|
|
|
|
return app.exec()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|