cleanup and files added

This commit is contained in:
2025-08-26 13:24:53 -07:00
parent a379d7a063
commit 51a14cd61c
8968 changed files with 1292619 additions and 0 deletions

View File

@@ -0,0 +1,390 @@
"""
Alert Widget - Displays system alerts and notifications
"""
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QFrame, QScrollArea, QCheckBox)
from PySide6.QtCore import Qt, Signal, QTimer, QDateTime, QPropertyAnimation, QEasingCurve
from PySide6.QtGui import QFont, QColor, QPalette
class AlertItem(QFrame):
"""Individual alert item widget"""
dismissed = Signal(str) # alert_id
action_triggered = Signal(str, str) # alert_id, action
def __init__(self, alert_id, alert_type, title, message, timestamp=None, parent=None):
super().__init__(parent)
self.alert_id = alert_id
self.alert_type = alert_type
self.timestamp = timestamp or QDateTime.currentDateTime()
self._setup_ui(title, message)
self._apply_style()
def _setup_ui(self, title, message):
"""Setup alert item UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(12, 8, 12, 8)
layout.setSpacing(4)
# Header with icon, title, and dismiss button
header_layout = QHBoxLayout()
# Alert icon
icons = {
'error': '🚨',
'warning': '⚠️',
'info': '',
'success': '',
'critical': '💥'
}
icon_label = QLabel(icons.get(self.alert_type, ''))
icon_label.setFont(QFont("Arial", 12))
header_layout.addWidget(icon_label)
# Title
title_label = QLabel(title)
title_label.setFont(QFont("Segoe UI", 9, QFont.Bold))
header_layout.addWidget(title_label)
header_layout.addStretch()
# Timestamp
time_label = QLabel(self.timestamp.toString("hh:mm:ss"))
time_label.setFont(QFont("Segoe UI", 8))
time_label.setStyleSheet("color: #7f8c8d;")
header_layout.addWidget(time_label)
# Dismiss button
dismiss_btn = QPushButton("")
dismiss_btn.setFixedSize(20, 20)
dismiss_btn.setStyleSheet("""
QPushButton {
background-color: transparent;
border: none;
color: #95a5a6;
font-size: 12px;
}
QPushButton:hover {
background-color: #ecf0f1;
border-radius: 10px;
}
""")
dismiss_btn.clicked.connect(lambda: self.dismissed.emit(self.alert_id))
header_layout.addWidget(dismiss_btn)
layout.addLayout(header_layout)
# Message
message_label = QLabel(message)
message_label.setFont(QFont("Segoe UI", 8))
message_label.setWordWrap(True)
message_label.setStyleSheet("color: #2c3e50; margin-left: 20px;")
layout.addWidget(message_label)
# Action buttons for critical/error alerts
if self.alert_type in ['error', 'critical']:
self._add_action_buttons(layout)
def _add_action_buttons(self, layout):
"""Add action buttons for alerts that require user action"""
actions_layout = QHBoxLayout()
actions_layout.setContentsMargins(20, 4, 0, 0)
# Acknowledge button
ack_btn = QPushButton("Acknowledge")
ack_btn.setStyleSheet("""
QPushButton {
background-color: #3498db;
color: white;
border: none;
padding: 4px 8px;
border-radius: 3px;
font-size: 8pt;
}
QPushButton:hover {
background-color: #2980b9;
}
""")
ack_btn.clicked.connect(lambda: self.action_triggered.emit(self.alert_id, "acknowledge"))
actions_layout.addWidget(ack_btn)
# View details button
details_btn = QPushButton("View Details")
details_btn.setStyleSheet("""
QPushButton {
background-color: #95a5a6;
color: white;
border: none;
padding: 4px 8px;
border-radius: 3px;
font-size: 8pt;
}
QPushButton:hover {
background-color: #7f8c8d;
}
""")
details_btn.clicked.connect(lambda: self.action_triggered.emit(self.alert_id, "details"))
actions_layout.addWidget(details_btn)
actions_layout.addStretch()
layout.addLayout(actions_layout)
def _apply_style(self):
"""Apply alert type specific styling"""
colors = {
'error': '#e74c3c',
'warning': '#f39c12',
'info': '#3498db',
'success': '#27ae60',
'critical': '#8e44ad'
}
bg_colors = {
'error': '#fdf2f2',
'warning': '#fef9e7',
'info': '#e8f4fd',
'success': '#eafaf1',
'critical': '#f4ecf7'
}
border_color = colors.get(self.alert_type, '#bdc3c7')
bg_color = bg_colors.get(self.alert_type, '#f8f9fa')
self.setStyleSheet(f"""
QFrame {{
background-color: {bg_color};
border-left: 4px solid {border_color};
border-radius: 4px;
margin: 2px 0px;
}}
""")
class AlertWidget(QWidget):
"""
Main alert widget for displaying system notifications and alerts
Features:
- Multiple alert types (error, warning, info, success, critical)
- Auto-dismiss for certain alert types
- Alert filtering and search
- Action buttons for critical alerts
- Alert history with timestamps
"""
# Signals
alert_action_required = Signal(str, str) # alert_id, action
alerts_cleared = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.alerts = {} # alert_id -> AlertItem
self.auto_dismiss_types = ['success', 'info']
self.max_alerts = 50
self._setup_ui()
# Auto-dismiss timer
self.dismiss_timer = QTimer()
self.dismiss_timer.timeout.connect(self._check_auto_dismiss)
self.dismiss_timer.start(5000) # Check every 5 seconds
def _setup_ui(self):
"""Setup alert widget UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
# Header
header = self._create_header()
layout.addWidget(header)
# Alerts scroll area
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.scroll_area.setStyleSheet("""
QScrollArea {
border: none;
background-color: white;
}
""")
# Alerts container
self.alerts_container = QWidget()
self.alerts_layout = QVBoxLayout(self.alerts_container)
self.alerts_layout.setContentsMargins(8, 4, 8, 4)
self.alerts_layout.setSpacing(2)
self.alerts_layout.addStretch() # Push alerts to top
self.scroll_area.setWidget(self.alerts_container)
layout.addWidget(self.scroll_area)
# No alerts message
self.no_alerts_label = QLabel("No active alerts")
self.no_alerts_label.setAlignment(Qt.AlignCenter)
self.no_alerts_label.setStyleSheet("""
QLabel {
color: #95a5a6;
font-style: italic;
padding: 20px;
}
""")
self.alerts_layout.insertWidget(0, self.no_alerts_label)
def _create_header(self):
"""Create alerts header with controls"""
header = QFrame()
header.setFixedHeight(40)
header.setStyleSheet("""
QFrame {
background-color: #34495e;
border-bottom: 1px solid #2c3e50;
}
""")
layout = QHBoxLayout(header)
layout.setContentsMargins(10, 5, 10, 5)
# Title
title = QLabel("🚨 System Alerts")
title.setFont(QFont("Segoe UI", 10, QFont.Bold))
title.setStyleSheet("color: white;")
layout.addWidget(title)
layout.addStretch()
# Alert count
self.count_label = QLabel("0 alerts")
self.count_label.setFont(QFont("Segoe UI", 8))
self.count_label.setStyleSheet("color: #ecf0f1;")
layout.addWidget(self.count_label)
# Clear all button
clear_btn = QPushButton("Clear All")
clear_btn.setStyleSheet("""
QPushButton {
background-color: #e74c3c;
color: white;
border: none;
padding: 4px 8px;
border-radius: 3px;
font-size: 8pt;
}
QPushButton:hover {
background-color: #c0392b;
}
""")
clear_btn.clicked.connect(self.clear_all_alerts)
layout.addWidget(clear_btn)
return header
def add_alert(self, alert_type, title, message, alert_id=None, auto_dismiss=None):
"""
Add a new alert
Args:
alert_type: 'error', 'warning', 'info', 'success', 'critical'
title: Alert title
message: Alert message
alert_id: Unique alert ID (auto-generated if None)
auto_dismiss: Auto-dismiss in seconds (None for default behavior)
"""
if alert_id is None:
alert_id = f"alert_{len(self.alerts)}_{QDateTime.currentMSecsSinceEpoch()}"
# Remove oldest alert if at max capacity
if len(self.alerts) >= self.max_alerts:
oldest_id = next(iter(self.alerts))
self.dismiss_alert(oldest_id)
# Create alert item
alert_item = AlertItem(alert_id, alert_type, title, message)
alert_item.dismissed.connect(self.dismiss_alert)
alert_item.action_triggered.connect(self._on_alert_action)
# Add to layout (insert before stretch)
self.alerts_layout.insertWidget(self.alerts_layout.count() - 1, alert_item)
self.alerts[alert_id] = alert_item
# Hide no alerts message
self.no_alerts_label.hide()
# Update count
self._update_count()
# Auto-dismiss if configured
if auto_dismiss or (auto_dismiss is None and alert_type in self.auto_dismiss_types):
dismiss_time = auto_dismiss if auto_dismiss else 10 # 10 seconds default
QTimer.singleShot(dismiss_time * 1000, lambda: self.dismiss_alert(alert_id))
# Scroll to top to show new alert
self.scroll_area.verticalScrollBar().setValue(0)
print(f"🚨 Alert added: {alert_type} - {title}")
return alert_id
def dismiss_alert(self, alert_id):
"""Dismiss a specific alert"""
if alert_id in self.alerts:
alert_item = self.alerts[alert_id]
self.alerts_layout.removeWidget(alert_item)
alert_item.deleteLater()
del self.alerts[alert_id]
# Show no alerts message if empty
if not self.alerts:
self.no_alerts_label.show()
self._update_count()
print(f"🚨 Alert dismissed: {alert_id}")
def clear_all_alerts(self):
"""Clear all alerts"""
for alert_id in list(self.alerts.keys()):
self.dismiss_alert(alert_id)
self.alerts_cleared.emit()
print("🚨 All alerts cleared")
def _on_alert_action(self, alert_id, action):
"""Handle alert action button clicks"""
self.alert_action_required.emit(alert_id, action)
# Auto-dismiss after acknowledgment
if action == "acknowledge":
self.dismiss_alert(alert_id)
def _check_auto_dismiss(self):
"""Check for alerts that should be auto-dismissed"""
current_time = QDateTime.currentDateTime()
for alert_id, alert_item in list(self.alerts.items()):
# Auto-dismiss info and success alerts after 30 seconds
if (alert_item.alert_type in self.auto_dismiss_types and
alert_item.timestamp.secsTo(current_time) > 30):
self.dismiss_alert(alert_id)
def _update_count(self):
"""Update alert count display"""
count = len(self.alerts)
self.count_label.setText(f"{count} alert{'s' if count != 1 else ''}")
def get_alert_count(self):
"""Get current alert count"""
return len(self.alerts)
def get_alerts_by_type(self, alert_type):
"""Get alerts of specific type"""
return [alert for alert in self.alerts.values()
if alert.alert_type == alert_type]
def has_critical_alerts(self):
"""Check if there are any critical or error alerts"""
return any(alert.alert_type in ['critical', 'error']
for alert in self.alerts.values())

View File

@@ -0,0 +1,362 @@
"""
Camera Control Panel Widget - Individual camera controls
"""
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QSlider, QSpinBox, QComboBox,
QCheckBox, QGroupBox, QGridLayout)
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QFont
class CameraControlPanel(QWidget):
"""
Individual camera control panel with adjustment capabilities
Features:
- Brightness/Contrast/Saturation controls
- Zoom and pan controls
- Recording controls
- Detection sensitivity
- ROI (Region of Interest) settings
"""
# Signals
setting_changed = Signal(str, str, object) # camera_id, setting_name, value
recording_toggled = Signal(str, bool) # camera_id, recording_state
snapshot_requested = Signal(str) # camera_id
def __init__(self, parent=None):
super().__init__(parent)
self.active_camera = None
self.camera_settings = {}
self._setup_ui()
print("🎛️ Camera Control Panel initialized")
def _setup_ui(self):
"""Setup the camera control panel UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(5, 5, 5, 5)
layout.setSpacing(8)
# Camera selection info
info_section = self._create_info_section()
layout.addWidget(info_section)
# Image adjustment controls
image_section = self._create_image_section()
layout.addWidget(image_section)
# Position controls
position_section = self._create_position_section()
layout.addWidget(position_section)
# Recording controls
recording_section = self._create_recording_section()
layout.addWidget(recording_section)
layout.addStretch()
def _create_info_section(self):
"""Create camera info section"""
section = QGroupBox("Camera Info")
layout = QVBoxLayout(section)
# Active camera display
self.camera_info_label = QLabel("No camera selected")
self.camera_info_label.setFont(QFont("Segoe UI", 9, QFont.Bold))
layout.addWidget(self.camera_info_label)
# Status
self.status_info_label = QLabel("Status: Offline")
layout.addWidget(self.status_info_label)
# Resolution
self.resolution_label = QLabel("Resolution: --")
layout.addWidget(self.resolution_label)
return section
def _create_image_section(self):
"""Create image adjustment section"""
section = QGroupBox("Image Adjustment")
layout = QGridLayout(section)
# Brightness
layout.addWidget(QLabel("Brightness:"), 0, 0)
self.brightness_slider = QSlider(Qt.Horizontal)
self.brightness_slider.setRange(-100, 100)
self.brightness_slider.setValue(0)
self.brightness_slider.valueChanged.connect(
lambda v: self._setting_changed("brightness", v)
)
layout.addWidget(self.brightness_slider, 0, 1)
self.brightness_label = QLabel("0")
layout.addWidget(self.brightness_label, 0, 2)
# Contrast
layout.addWidget(QLabel("Contrast:"), 1, 0)
self.contrast_slider = QSlider(Qt.Horizontal)
self.contrast_slider.setRange(-100, 100)
self.contrast_slider.setValue(0)
self.contrast_slider.valueChanged.connect(
lambda v: self._setting_changed("contrast", v)
)
layout.addWidget(self.contrast_slider, 1, 1)
self.contrast_label = QLabel("0")
layout.addWidget(self.contrast_label, 1, 2)
# Saturation
layout.addWidget(QLabel("Saturation:"), 2, 0)
self.saturation_slider = QSlider(Qt.Horizontal)
self.saturation_slider.setRange(-100, 100)
self.saturation_slider.setValue(0)
self.saturation_slider.valueChanged.connect(
lambda v: self._setting_changed("saturation", v)
)
layout.addWidget(self.saturation_slider, 2, 1)
self.saturation_label = QLabel("0")
layout.addWidget(self.saturation_label, 2, 2)
# Auto adjust button
auto_btn = QPushButton("Auto Adjust")
auto_btn.clicked.connect(self._auto_adjust)
layout.addWidget(auto_btn, 3, 0, 1, 3)
# Connect slider value updates to labels
self.brightness_slider.valueChanged.connect(
lambda v: self.brightness_label.setText(str(v))
)
self.contrast_slider.valueChanged.connect(
lambda v: self.contrast_label.setText(str(v))
)
self.saturation_slider.valueChanged.connect(
lambda v: self.saturation_label.setText(str(v))
)
return section
def _create_position_section(self):
"""Create position and zoom controls"""
section = QGroupBox("Position & Zoom")
layout = QGridLayout(section)
# Zoom control
layout.addWidget(QLabel("Zoom:"), 0, 0)
self.zoom_slider = QSlider(Qt.Horizontal)
self.zoom_slider.setRange(100, 500) # 100% to 500%
self.zoom_slider.setValue(100)
self.zoom_slider.valueChanged.connect(
lambda v: self._setting_changed("zoom", v)
)
layout.addWidget(self.zoom_slider, 0, 1)
self.zoom_label = QLabel("100%")
layout.addWidget(self.zoom_label, 0, 2)
# Pan controls
pan_layout = QHBoxLayout()
# Pan buttons
pan_up_btn = QPushButton("")
pan_up_btn.setFixedSize(30, 30)
pan_up_btn.clicked.connect(lambda: self._pan_camera("up"))
pan_down_btn = QPushButton("")
pan_down_btn.setFixedSize(30, 30)
pan_down_btn.clicked.connect(lambda: self._pan_camera("down"))
pan_left_btn = QPushButton("")
pan_left_btn.setFixedSize(30, 30)
pan_left_btn.clicked.connect(lambda: self._pan_camera("left"))
pan_right_btn = QPushButton("")
pan_right_btn.setFixedSize(30, 30)
pan_right_btn.clicked.connect(lambda: self._pan_camera("right"))
# Arrange pan buttons in cross pattern
pan_grid = QGridLayout()
pan_grid.addWidget(pan_up_btn, 0, 1)
pan_grid.addWidget(pan_left_btn, 1, 0)
pan_grid.addWidget(pan_right_btn, 1, 2)
pan_grid.addWidget(pan_down_btn, 2, 1)
# Reset button in center
reset_btn = QPushButton("")
reset_btn.setFixedSize(30, 30)
reset_btn.setToolTip("Reset to center")
reset_btn.clicked.connect(self._reset_position)
pan_grid.addWidget(reset_btn, 1, 1)
layout.addLayout(pan_grid, 1, 0, 1, 3)
# Connect zoom slider to label
self.zoom_slider.valueChanged.connect(
lambda v: self.zoom_label.setText(f"{v}%")
)
return section
def _create_recording_section(self):
"""Create recording controls"""
section = QGroupBox("Recording Controls")
layout = QVBoxLayout(section)
# Recording toggle
self.record_btn = QPushButton("🔴 Start Recording")
self.record_btn.clicked.connect(self._toggle_recording)
layout.addWidget(self.record_btn)
# Snapshot button
snapshot_btn = QPushButton("📸 Take Snapshot")
snapshot_btn.clicked.connect(self._take_snapshot)
layout.addWidget(snapshot_btn)
# Recording settings
settings_layout = QGridLayout()
# Quality setting
settings_layout.addWidget(QLabel("Quality:"), 0, 0)
self.quality_combo = QComboBox()
self.quality_combo.addItems(["Low", "Medium", "High", "Ultra"])
self.quality_combo.setCurrentText("High")
self.quality_combo.currentTextChanged.connect(
lambda v: self._setting_changed("quality", v)
)
settings_layout.addWidget(self.quality_combo, 0, 1)
# Frame rate
settings_layout.addWidget(QLabel("FPS:"), 1, 0)
self.fps_spinbox = QSpinBox()
self.fps_spinbox.setRange(1, 60)
self.fps_spinbox.setValue(30)
self.fps_spinbox.valueChanged.connect(
lambda v: self._setting_changed("fps", v)
)
settings_layout.addWidget(self.fps_spinbox, 1, 1)
layout.addLayout(settings_layout)
# Recording status
self.recording_status_label = QLabel("Not recording")
self.recording_status_label.setStyleSheet("color: gray; font-style: italic;")
layout.addWidget(self.recording_status_label)
return section
def set_active_camera(self, camera_id):
"""Set the active camera for controls"""
self.active_camera = camera_id
# Update display
display_name = camera_id.replace('_', ' ').title()
self.camera_info_label.setText(f"Camera: {display_name}")
# Load camera settings if available
if camera_id in self.camera_settings:
self._load_camera_settings(camera_id)
else:
self._reset_all_settings()
print(f"🎛️ Active camera set to: {camera_id}")
def _setting_changed(self, setting_name, value):
"""Handle setting change"""
if self.active_camera:
# Store setting
if self.active_camera not in self.camera_settings:
self.camera_settings[self.active_camera] = {}
self.camera_settings[self.active_camera][setting_name] = value
# Emit signal
self.setting_changed.emit(self.active_camera, setting_name, value)
def _toggle_recording(self):
"""Toggle recording for active camera"""
if not self.active_camera:
return
is_recording = self.record_btn.text().startswith("🔴")
if is_recording:
# Start recording
self.record_btn.setText("⏹️ Stop Recording")
self.recording_status_label.setText("Recording...")
self.recording_status_label.setStyleSheet("color: red; font-weight: bold;")
self.recording_toggled.emit(self.active_camera, True)
else:
# Stop recording
self.record_btn.setText("🔴 Start Recording")
self.recording_status_label.setText("Not recording")
self.recording_status_label.setStyleSheet("color: gray; font-style: italic;")
self.recording_toggled.emit(self.active_camera, False)
def _take_snapshot(self):
"""Take snapshot of active camera"""
if self.active_camera:
self.snapshot_requested.emit(self.active_camera)
def _auto_adjust(self):
"""Auto-adjust image settings"""
if self.active_camera:
# Reset to defaults with slight improvements
self.brightness_slider.setValue(10)
self.contrast_slider.setValue(15)
self.saturation_slider.setValue(5)
print(f"🎛️ Auto-adjusted settings for {self.active_camera}")
def _pan_camera(self, direction):
"""Pan camera in specified direction"""
if self.active_camera:
self._setting_changed(f"pan_{direction}", True)
print(f"🎛️ Panning camera {direction}")
def _reset_position(self):
"""Reset camera position to center"""
if self.active_camera:
self.zoom_slider.setValue(100)
self._setting_changed("reset_position", True)
print(f"🎛️ Reset position for {self.active_camera}")
def _load_camera_settings(self, camera_id):
"""Load stored settings for camera"""
settings = self.camera_settings.get(camera_id, {})
# Apply settings to controls
self.brightness_slider.setValue(settings.get("brightness", 0))
self.contrast_slider.setValue(settings.get("contrast", 0))
self.saturation_slider.setValue(settings.get("saturation", 0))
self.zoom_slider.setValue(settings.get("zoom", 100))
self.quality_combo.setCurrentText(settings.get("quality", "High"))
self.fps_spinbox.setValue(settings.get("fps", 30))
def _reset_all_settings(self):
"""Reset all controls to default values"""
self.brightness_slider.setValue(0)
self.contrast_slider.setValue(0)
self.saturation_slider.setValue(0)
self.zoom_slider.setValue(100)
self.quality_combo.setCurrentText("High")
self.fps_spinbox.setValue(30)
def update_camera_status(self, camera_id, status, resolution=None):
"""Update camera status information"""
if camera_id == self.active_camera:
self.status_info_label.setText(f"Status: {status.title()}")
if resolution:
self.resolution_label.setText(f"Resolution: {resolution}")
# Update status color
if status == "online":
self.status_info_label.setStyleSheet("color: green;")
elif status == "offline":
self.status_info_label.setStyleSheet("color: red;")
else:
self.status_info_label.setStyleSheet("color: orange;")

View File

@@ -0,0 +1,171 @@
"""
Detection Overlay Widget - Displays object detection bounding boxes and labels
"""
from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QFrame
from PySide6.QtCore import Qt, Signal, QRect, QPoint, QTimer
from PySide6.QtGui import QPainter, QPen, QBrush, QColor, QFont
import json
class DetectionOverlayWidget(QWidget):
"""
Widget for displaying object detection overlays on video frames
Features:
- Bounding box visualization
- Class labels and confidence scores
- Tracking IDs for multi-object tracking
- Color-coded detection classes
- Customizable overlay styles
"""
# Signals
detection_clicked = Signal(dict) # Emitted when a detection is clicked
def __init__(self, parent=None):
super().__init__(parent)
self.detections = []
self.class_colors = {
'person': QColor(255, 0, 0), # Red
'bicycle': QColor(0, 255, 0), # Green
'car': QColor(0, 0, 255), # Blue
'motorcycle': QColor(255, 255, 0), # Yellow
'bus': QColor(255, 0, 255), # Magenta
'truck': QColor(0, 255, 255), # Cyan
'traffic_light': QColor(255, 165, 0), # Orange
'stop_sign': QColor(128, 0, 128), # Purple
}
self.show_labels = True
self.show_confidence = True
self.show_tracking_id = True
self.overlay_opacity = 0.8
# Make widget transparent for overlay
self.setAttribute(Qt.WA_TransparentForMouseEvents, False)
self.setStyleSheet("background-color: transparent;")
def set_detections(self, detections):
"""
Set detection results to display
Args:
detections: List of detection dictionaries with format:
{
'bbox': [x1, y1, x2, y2],
'class': 'person',
'confidence': 0.95,
'track_id': 123 (optional)
}
"""
self.detections = detections
self.update() # Trigger repaint
def set_overlay_options(self, show_labels=True, show_confidence=True,
show_tracking_id=True, opacity=0.8):
"""Configure overlay display options"""
self.show_labels = show_labels
self.show_confidence = show_confidence
self.show_tracking_id = show_tracking_id
self.overlay_opacity = opacity
self.update()
def add_class_color(self, class_name, color):
"""Add or update color for a detection class"""
self.class_colors[class_name] = color
def paintEvent(self, event):
"""Paint detection overlays"""
if not self.detections:
return
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# Set up font for labels
font = QFont("Arial", 10, QFont.Bold)
painter.setFont(font)
for detection in self.detections:
self._draw_detection(painter, detection)
def _draw_detection(self, painter, detection):
"""Draw a single detection"""
# Extract detection info
bbox = detection.get('bbox', [0, 0, 100, 100])
class_name = detection.get('class', 'unknown')
confidence = detection.get('confidence', 0.0)
track_id = detection.get('track_id', None)
x1, y1, x2, y2 = bbox
# Get color for this class
color = self.class_colors.get(class_name, QColor(128, 128, 128))
# Set up pen for bounding box
pen = QPen(color, 3)
painter.setPen(pen)
# Draw bounding box
rect = QRect(int(x1), int(y1), int(x2-x1), int(y2-y1))
painter.drawRect(rect)
# Prepare label text
label_parts = []
if self.show_labels:
label_parts.append(class_name)
if self.show_confidence:
label_parts.append(f"{confidence:.2f}")
if self.show_tracking_id and track_id is not None:
label_parts.append(f"ID:{track_id}")
if not label_parts:
return
label_text = " | ".join(label_parts)
# Calculate label background
metrics = painter.fontMetrics()
label_width = metrics.horizontalAdvance(label_text) + 10
label_height = metrics.height() + 6
# Draw label background
label_rect = QRect(int(x1), int(y1-label_height), label_width, label_height)
painter.fillRect(label_rect, color)
# Draw label text
painter.setPen(QPen(QColor(255, 255, 255), 1))
text_rect = QRect(int(x1+5), int(y1-label_height+3), label_width-10, label_height-6)
painter.drawText(text_rect, Qt.AlignLeft | Qt.AlignVCenter, label_text)
def mousePressEvent(self, event):
"""Handle mouse clicks on detections"""
if event.button() == Qt.LeftButton:
click_pos = event.pos()
# Check if click is within any detection bbox
for detection in self.detections:
bbox = detection.get('bbox', [0, 0, 100, 100])
x1, y1, x2, y2 = bbox
if x1 <= click_pos.x() <= x2 and y1 <= click_pos.y() <= y2:
self.detection_clicked.emit(detection)
break
def clear_detections(self):
"""Clear all detections"""
self.detections = []
self.update()
def get_detection_count(self):
"""Get number of current detections"""
return len(self.detections)
def get_class_counts(self):
"""Get count of detections by class"""
counts = {}
for detection in self.detections:
class_name = detection.get('class', 'unknown')
counts[class_name] = counts.get(class_name, 0) + 1
return counts

View File

@@ -0,0 +1,280 @@
"""
Notification Center Widget - Modern notification panel with filtering and management
"""
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QScrollArea, QFrame, QComboBox,
QLineEdit, QCheckBox, QSizePolicy)
from PySide6.QtCore import Qt, Signal, QTimer, QDateTime
from PySide6.QtGui import QFont, QIcon
class NotificationItem(QFrame):
"""Individual notification item with modern styling"""
dismissed = Signal(object) # Emitted when notification is dismissed
def __init__(self, message, level="info", timestamp=None, parent=None):
super().__init__(parent)
self.message = message
self.level = level
self.timestamp = timestamp or QDateTime.currentDateTime()
self.setObjectName(f"notificationItem_{level}")
self.setFrameStyle(QFrame.Box)
self.setMaximumHeight(80)
self._setup_ui()
self._apply_style()
# Auto-dismiss timer for info notifications
if level == "info":
QTimer.singleShot(10000, self._auto_dismiss) # 10 seconds
def _setup_ui(self):
"""Setup the notification item UI"""
layout = QHBoxLayout(self)
layout.setContentsMargins(10, 8, 10, 8)
# Level icon
icon_label = QLabel()
icon_label.setFixedSize(24, 24)
icon_label.setAlignment(Qt.AlignCenter)
icons = {
'info': '💡',
'warning': '⚠️',
'error': '',
'success': ''
}
icon_label.setText(icons.get(self.level, '📋'))
icon_label.setStyleSheet("font-size: 16px;")
layout.addWidget(icon_label)
# Content area
content_layout = QVBoxLayout()
# Message
message_label = QLabel(self.message)
message_label.setWordWrap(True)
message_label.setFont(QFont("Segoe UI", 9))
content_layout.addWidget(message_label)
# Timestamp
time_label = QLabel(self.timestamp.toString("hh:mm:ss"))
time_label.setFont(QFont("Segoe UI", 8))
time_label.setStyleSheet("color: gray;")
content_layout.addWidget(time_label)
layout.addLayout(content_layout, 1)
# Dismiss button
dismiss_btn = QPushButton("×")
dismiss_btn.setFixedSize(20, 20)
dismiss_btn.setFont(QFont("Arial", 10, QFont.Bold))
dismiss_btn.clicked.connect(self._dismiss)
layout.addWidget(dismiss_btn)
def _apply_style(self):
"""Apply level-specific styling"""
styles = {
'info': "border-left: 3px solid #3498db; background-color: rgba(52, 152, 219, 0.1);",
'warning': "border-left: 3px solid #f39c12; background-color: rgba(243, 156, 18, 0.1);",
'error': "border-left: 3px solid #e74c3c; background-color: rgba(231, 76, 60, 0.1);",
'success': "border-left: 3px solid #27ae60; background-color: rgba(39, 174, 96, 0.1);"
}
self.setStyleSheet(styles.get(self.level, styles['info']))
def _dismiss(self):
"""Dismiss this notification"""
self.dismissed.emit(self)
def _auto_dismiss(self):
"""Auto-dismiss for info notifications"""
if self.level == "info":
self._dismiss()
class NotificationCenter(QWidget):
"""
Modern notification center with filtering and management capabilities
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedWidth(350)
self.notifications = []
self.max_notifications = 50
self._setup_ui()
print("✅ Notification Center initialized")
def _setup_ui(self):
"""Setup the notification center UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(5)
# Header
header = self._create_header()
layout.addWidget(header)
# Filter controls
filters = self._create_filters()
layout.addWidget(filters)
# Notification list
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarNever)
self.notifications_widget = QWidget()
self.notifications_layout = QVBoxLayout(self.notifications_widget)
self.notifications_layout.setAlignment(Qt.AlignTop)
self.scroll_area.setWidget(self.notifications_widget)
layout.addWidget(self.scroll_area)
# Footer with actions
footer = self._create_footer()
layout.addWidget(footer)
def _create_header(self):
"""Create the header with title and controls"""
header = QFrame()
header.setFixedHeight(40)
layout = QHBoxLayout(header)
# Title
title = QLabel("Notifications")
title.setFont(QFont("Segoe UI", 11, QFont.Bold))
layout.addWidget(title)
layout.addStretch()
# Notification count
self.count_label = QLabel("0")
self.count_label.setFont(QFont("Segoe UI", 9))
self.count_label.setStyleSheet("color: gray;")
layout.addWidget(self.count_label)
return header
def _create_filters(self):
"""Create filter controls"""
filters = QFrame()
filters.setFixedHeight(35)
layout = QHBoxLayout(filters)
layout.setContentsMargins(0, 0, 0, 0)
# Level filter
self.level_filter = QComboBox()
self.level_filter.addItems(["All", "Info", "Warning", "Error", "Success"])
self.level_filter.currentTextChanged.connect(self._apply_filters)
layout.addWidget(self.level_filter)
# Search filter
self.search_filter = QLineEdit()
self.search_filter.setPlaceholderText("Search notifications...")
self.search_filter.textChanged.connect(self._apply_filters)
layout.addWidget(self.search_filter)
return filters
def _create_footer(self):
"""Create footer with action buttons"""
footer = QFrame()
footer.setFixedHeight(35)
layout = QHBoxLayout(footer)
# Clear all button
clear_btn = QPushButton("Clear All")
clear_btn.clicked.connect(self.clear_all)
layout.addWidget(clear_btn)
layout.addStretch()
# Export button
export_btn = QPushButton("Export")
export_btn.clicked.connect(self._export_notifications)
layout.addWidget(export_btn)
return footer
def add_notification(self, message, level="info"):
"""Add a new notification"""
# Remove oldest notification if at limit
if len(self.notifications) >= self.max_notifications:
oldest = self.notifications[0]
self._remove_notification(oldest)
# Create new notification
notification = NotificationItem(message, level)
notification.dismissed.connect(self._remove_notification)
# Add to list and layout
self.notifications.append(notification)
self.notifications_layout.insertWidget(0, notification) # Add to top
# Update count
self._update_count()
# Scroll to top to show new notification
self.scroll_area.verticalScrollBar().setValue(0)
print(f"📝 Notification added: {level.upper()} - {message[:50]}...")
def _remove_notification(self, notification):
"""Remove a notification"""
if notification in self.notifications:
self.notifications.remove(notification)
notification.setParent(None)
self._update_count()
def _apply_filters(self):
"""Apply current filters to notifications"""
level_filter = self.level_filter.currentText().lower()
search_text = self.search_filter.text().lower()
for notification in self.notifications:
# Check level filter
level_match = (level_filter == "all" or
notification.level == level_filter)
# Check search filter
search_match = (not search_text or
search_text in notification.message.lower())
# Show/hide notification
notification.setVisible(level_match and search_match)
def _update_count(self):
"""Update the notification count"""
visible_count = sum(1 for n in self.notifications if n.isVisible())
total_count = len(self.notifications)
self.count_label.setText(f"{visible_count}/{total_count}")
def clear_all(self):
"""Clear all notifications"""
for notification in self.notifications[:]: # Copy list to avoid modification during iteration
self._remove_notification(notification)
def _export_notifications(self):
"""Export notifications to file"""
# Implementation for exporting notifications
print("📤 Exporting notifications...")
def get_notification_summary(self):
"""Get summary of current notifications"""
summary = {
'total': len(self.notifications),
'by_level': {}
}
for notification in self.notifications:
level = notification.level
summary['by_level'][level] = summary['by_level'].get(level, 0) + 1
return summary

View File

@@ -0,0 +1,290 @@
"""
Statistics Panel Widget - Real-time traffic statistics display
"""
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QFrame, QProgressBar, QGridLayout)
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QFont, QColor, QPalette
class StatisticsPanel(QWidget):
"""
Real-time statistics panel for traffic monitoring
Features:
- Vehicle counts by type
- Traffic flow rates
- Violation statistics
- Performance metrics
- Historical trends
"""
def __init__(self, parent=None):
super().__init__(parent)
self.stats_data = {
'vehicle_count': 0,
'car_count': 0,
'truck_count': 0,
'motorcycle_count': 0,
'person_count': 0,
'violation_count': 0,
'avg_speed': 0.0,
'traffic_density': 0.0,
'fps': 0.0,
'processing_time': 0.0
}
self._setup_ui()
print("📊 Statistics Panel initialized")
def _setup_ui(self):
"""Setup the statistics panel UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(5, 5, 5, 5)
layout.setSpacing(10)
# Vehicle counts section
vehicle_section = self._create_vehicle_section()
layout.addWidget(vehicle_section)
# Traffic metrics section
metrics_section = self._create_metrics_section()
layout.addWidget(metrics_section)
# Performance section
performance_section = self._create_performance_section()
layout.addWidget(performance_section)
layout.addStretch()
def _create_vehicle_section(self):
"""Create vehicle statistics section"""
section = QFrame()
section.setFrameStyle(QFrame.Box)
section.setStyleSheet("""
QFrame {
border: 1px solid #bdc3c7;
border-radius: 6px;
background-color: rgba(236, 240, 241, 0.3);
}
""")
layout = QVBoxLayout(section)
layout.setContentsMargins(8, 8, 8, 8)
# Section title
title = QLabel("🚗 Vehicle Detection")
title.setFont(QFont("Segoe UI", 9, QFont.Bold))
layout.addWidget(title)
# Vehicle type grid
grid = QGridLayout()
grid.setSpacing(5)
# Total vehicles
self.total_label = self._create_stat_label("Total", "0")
grid.addWidget(QLabel("Total:"), 0, 0)
grid.addWidget(self.total_label, 0, 1)
# Cars
self.cars_label = self._create_stat_label("Cars", "0")
grid.addWidget(QLabel("Cars:"), 1, 0)
grid.addWidget(self.cars_label, 1, 1)
# Trucks
self.trucks_label = self._create_stat_label("Trucks", "0")
grid.addWidget(QLabel("Trucks:"), 2, 0)
grid.addWidget(self.trucks_label, 2, 1)
# Motorcycles
self.motorcycles_label = self._create_stat_label("Motorcycles", "0")
grid.addWidget(QLabel("Motorcycles:"), 3, 0)
grid.addWidget(self.motorcycles_label, 3, 1)
# Pedestrians
self.pedestrians_label = self._create_stat_label("Pedestrians", "0")
grid.addWidget(QLabel("Pedestrians:"), 4, 0)
grid.addWidget(self.pedestrians_label, 4, 1)
layout.addLayout(grid)
return section
def _create_metrics_section(self):
"""Create traffic metrics section"""
section = QFrame()
section.setFrameStyle(QFrame.Box)
section.setStyleSheet("""
QFrame {
border: 1px solid #bdc3c7;
border-radius: 6px;
background-color: rgba(52, 152, 219, 0.1);
}
""")
layout = QVBoxLayout(section)
layout.setContentsMargins(8, 8, 8, 8)
# Section title
title = QLabel("📈 Traffic Metrics")
title.setFont(QFont("Segoe UI", 9, QFont.Bold))
layout.addWidget(title)
# Metrics grid
grid = QGridLayout()
grid.setSpacing(5)
# Average speed
self.speed_label = self._create_stat_label("Speed", "0.0 km/h")
grid.addWidget(QLabel("Avg Speed:"), 0, 0)
grid.addWidget(self.speed_label, 0, 1)
# Traffic density
self.density_label = self._create_stat_label("Density", "Low")
grid.addWidget(QLabel("Density:"), 1, 0)
grid.addWidget(self.density_label, 1, 1)
# Violations
self.violations_label = self._create_stat_label("Violations", "0")
grid.addWidget(QLabel("Violations:"), 2, 0)
grid.addWidget(self.violations_label, 2, 1)
layout.addLayout(grid)
# Traffic density bar
density_layout = QHBoxLayout()
density_layout.addWidget(QLabel("Traffic Flow:"))
self.density_bar = QProgressBar()
self.density_bar.setRange(0, 100)
self.density_bar.setValue(0)
self.density_bar.setMaximumHeight(15)
density_layout.addWidget(self.density_bar)
layout.addLayout(density_layout)
return section
def _create_performance_section(self):
"""Create performance metrics section"""
section = QFrame()
section.setFrameStyle(QFrame.Box)
section.setStyleSheet("""
QFrame {
border: 1px solid #bdc3c7;
border-radius: 6px;
background-color: rgba(39, 174, 96, 0.1);
}
""")
layout = QVBoxLayout(section)
layout.setContentsMargins(8, 8, 8, 8)
# Section title
title = QLabel("⚡ Performance")
title.setFont(QFont("Segoe UI", 9, QFont.Bold))
layout.addWidget(title)
# Performance grid
grid = QGridLayout()
grid.setSpacing(5)
# FPS
self.fps_label = self._create_stat_label("FPS", "0.0")
grid.addWidget(QLabel("FPS:"), 0, 0)
grid.addWidget(self.fps_label, 0, 1)
# Processing time
self.proc_time_label = self._create_stat_label("Processing", "0.0 ms")
grid.addWidget(QLabel("Proc Time:"), 1, 0)
grid.addWidget(self.proc_time_label, 1, 1)
layout.addLayout(grid)
# FPS progress bar
fps_layout = QHBoxLayout()
fps_layout.addWidget(QLabel("FPS Health:"))
self.fps_bar = QProgressBar()
self.fps_bar.setRange(0, 30) # Target 30 FPS
self.fps_bar.setValue(0)
self.fps_bar.setMaximumHeight(15)
fps_layout.addWidget(self.fps_bar)
layout.addLayout(fps_layout)
return section
def _create_stat_label(self, name, value):
"""Create a styled statistics label"""
label = QLabel(value)
label.setFont(QFont("Segoe UI", 9, QFont.Bold))
label.setAlignment(Qt.AlignRight)
label.setStyleSheet("color: #2c3e50; padding: 2px;")
return label
def update_data(self, new_data):
"""Update statistics with new data"""
# Update internal data
self.stats_data.update(new_data)
# Update vehicle counts
self.total_label.setText(str(self.stats_data.get('vehicle_count', 0)))
self.cars_label.setText(str(self.stats_data.get('car_count', 0)))
self.trucks_label.setText(str(self.stats_data.get('truck_count', 0)))
self.motorcycles_label.setText(str(self.stats_data.get('motorcycle_count', 0)))
self.pedestrians_label.setText(str(self.stats_data.get('person_count', 0)))
# Update traffic metrics
avg_speed = self.stats_data.get('avg_speed', 0.0)
self.speed_label.setText(f"{avg_speed:.1f} km/h")
# Update traffic density
density = self.stats_data.get('traffic_density', 0.0)
density_text = self._get_density_text(density)
self.density_label.setText(density_text)
self.density_bar.setValue(int(density * 100))
# Update violations
violations = self.stats_data.get('violation_count', 0)
self.violations_label.setText(str(violations))
# Update performance metrics
fps = self.stats_data.get('fps', 0.0)
self.fps_label.setText(f"{fps:.1f}")
self.fps_bar.setValue(int(fps))
# Style FPS bar based on performance
if fps >= 25:
self.fps_bar.setStyleSheet("QProgressBar::chunk { background-color: #27ae60; }")
elif fps >= 15:
self.fps_bar.setStyleSheet("QProgressBar::chunk { background-color: #f39c12; }")
else:
self.fps_bar.setStyleSheet("QProgressBar::chunk { background-color: #e74c3c; }")
proc_time = self.stats_data.get('processing_time', 0.0)
self.proc_time_label.setText(f"{proc_time:.1f} ms")
def _get_density_text(self, density):
"""Convert density value to text description"""
if density < 0.3:
return "Low"
elif density < 0.6:
return "Medium"
elif density < 0.8:
return "High"
else:
return "Very High"
def get_current_stats(self):
"""Get current statistics data"""
return self.stats_data.copy()
def reset_stats(self):
"""Reset all statistics to zero"""
self.stats_data = {key: 0 if isinstance(value, (int, float)) else value
for key, value in self.stats_data.items()}
self.update_data({})
print("📊 Statistics reset")

View File

@@ -0,0 +1,104 @@
"""
Status Indicator Widget - Modern status display with animations
"""
from PySide6.QtWidgets import QWidget, QHBoxLayout, QLabel, QFrame
from PySide6.QtCore import Qt, QTimer, QPropertyAnimation, QEasingCurve, Property
from PySide6.QtGui import QPainter, QColor, QFont
class StatusIndicator(QWidget):
"""
Modern status indicator with animated status changes
States:
- running (green pulse)
- stopped (red solid)
- warning (yellow blink)
- error (red blink)
- loading (blue pulse)
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedSize(24, 24)
self._status = "stopped"
self._opacity = 1.0
# Animation for pulsing/blinking effects
self.animation = QPropertyAnimation(self, b"opacity")
self.animation.setDuration(1000)
self.animation.setEasingCurve(QEasingCurve.InOutQuad)
# Status colors
self.colors = {
'running': QColor('#27ae60'), # Green
'stopped': QColor('#e74c3c'), # Red
'warning': QColor('#f39c12'), # Orange
'error': QColor('#c0392b'), # Dark red
'loading': QColor('#3498db'), # Blue
}
print("✅ Status Indicator initialized")
def get_opacity(self):
return self._opacity
def set_opacity(self, opacity):
self._opacity = opacity
self.update()
opacity = Property(float, get_opacity, set_opacity)
def set_status(self, status):
"""Set the status and start appropriate animation"""
if status not in self.colors:
status = 'stopped'
self._status = status
# Configure animation based on status
if status == 'running':
# Slow pulse for running
self.animation.setStartValue(0.3)
self.animation.setEndValue(1.0)
self.animation.setLoopCount(-1) # Infinite
self.animation.start()
elif status in ['warning', 'error']:
# Fast blink for alerts
self.animation.setStartValue(0.2)
self.animation.setEndValue(1.0)
self.animation.setDuration(500)
self.animation.setLoopCount(-1)
self.animation.start()
elif status == 'loading':
# Medium pulse for loading
self.animation.setStartValue(0.4)
self.animation.setEndValue(1.0)
self.animation.setDuration(800)
self.animation.setLoopCount(-1)
self.animation.start()
else:
# Solid color for stopped
self.animation.stop()
self._opacity = 1.0
self.update()
def paintEvent(self, event):
"""Custom paint event for the status indicator"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# Get current color
color = self.colors.get(self._status, self.colors['stopped'])
color.setAlphaF(self._opacity)
# Draw the status circle
painter.setBrush(color)
painter.setPen(Qt.NoPen)
painter.drawEllipse(2, 2, 20, 20)
# Add inner highlight for 3D effect
highlight = QColor(255, 255, 255, int(80 * self._opacity))
painter.setBrush(highlight)
painter.drawEllipse(4, 4, 8, 8)

View File

@@ -0,0 +1,315 @@
"""
Video Display Widget - Modern video player with detection overlays
"""
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFrame
from PySide6.QtCore import Qt, Signal, QTimer, QRect
from PySide6.QtGui import QPainter, QPixmap, QColor, QFont, QPen, QBrush
class VideoDisplayWidget(QWidget):
"""
Modern video display widget with detection overlays and controls
Features:
- Video frame display with aspect ratio preservation
- Detection bounding boxes with confidence scores
- Tracking ID display
- Recording indicator
- Camera status overlay
- Fullscreen support via double-click
"""
# Signals
double_clicked = Signal()
recording_toggled = Signal(bool)
snapshot_requested = Signal()
def __init__(self, camera_id="camera_1", parent=None):
super().__init__(parent)
self.camera_id = camera_id
self.current_frame = None
self.detections = []
self.confidence_threshold = 0.5
self.is_recording = False
self.camera_status = "offline"
# Overlay settings
self.overlay_settings = {
'show_boxes': True,
'show_tracks': True,
'show_speed': False,
'show_confidence': True
}
# Colors for different detection classes
self.class_colors = {
'car': QColor('#3498db'), # Blue
'truck': QColor('#e74c3c'), # Red
'bus': QColor('#f39c12'), # Orange
'motorcycle': QColor('#9b59b6'), # Purple
'bicycle': QColor('#1abc9c'), # Turquoise
'person': QColor('#2ecc71'), # Green
'default': QColor('#95a5a6') # Gray
}
self._setup_ui()
print(f"📺 Video Display Widget initialized for {camera_id}")
def _setup_ui(self):
"""Setup the video display UI"""
layout = QVBoxLayout(self)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(2)
# Header with camera info
header = self._create_header()
layout.addWidget(header)
# Video display area
self.video_frame = QFrame()
self.video_frame.setMinimumSize(320, 240)
self.video_frame.setStyleSheet("""
QFrame {
background-color: #2c3e50;
border: 1px solid #34495e;
border-radius: 4px;
}
""")
layout.addWidget(self.video_frame, 1)
# Footer with controls
footer = self._create_footer()
layout.addWidget(footer)
def _create_header(self):
"""Create header with camera info"""
header = QFrame()
header.setFixedHeight(25)
header.setStyleSheet("background-color: rgba(0, 0, 0, 0.7); border-radius: 2px;")
layout = QHBoxLayout(header)
layout.setContentsMargins(5, 2, 5, 2)
# Camera name
self.camera_label = QLabel(self.camera_id.replace('_', ' ').title())
self.camera_label.setStyleSheet("color: white; font-weight: bold; font-size: 8pt;")
layout.addWidget(self.camera_label)
layout.addStretch()
# Status indicator
self.status_label = QLabel("")
self.status_label.setStyleSheet("color: #e74c3c; font-size: 10pt;")
layout.addWidget(self.status_label)
# Recording indicator
self.recording_indicator = QLabel()
self.recording_indicator.setStyleSheet("color: #e74c3c; font-size: 8pt;")
self.recording_indicator.hide()
layout.addWidget(self.recording_indicator)
return header
def _create_footer(self):
"""Create footer with quick controls"""
footer = QFrame()
footer.setFixedHeight(20)
footer.setStyleSheet("background-color: rgba(0, 0, 0, 0.5); border-radius: 2px;")
layout = QHBoxLayout(footer)
layout.setContentsMargins(2, 1, 2, 1)
# Quick action buttons (small)
snapshot_btn = QPushButton("📸")
snapshot_btn.setFixedSize(16, 16)
snapshot_btn.setStyleSheet("""
QPushButton {
background: transparent;
border: none;
color: white;
font-size: 6pt;
}
QPushButton:hover {
background-color: rgba(255, 255, 255, 0.2);
border-radius: 2px;
}
""")
snapshot_btn.clicked.connect(self.take_snapshot)
layout.addWidget(snapshot_btn)
layout.addStretch()
# FPS counter
self.fps_label = QLabel("-- fps")
self.fps_label.setStyleSheet("color: white; font-size: 6pt;")
layout.addWidget(self.fps_label)
return footer
def set_frame(self, frame):
"""Set the current video frame"""
self.current_frame = frame
self.update() # Trigger repaint
def add_detections(self, detections):
"""Add detection results to display"""
self.detections = detections
self.update() # Trigger repaint
def set_confidence_threshold(self, threshold):
"""Set confidence threshold for detection display"""
self.confidence_threshold = threshold
self.update()
def update_overlay_settings(self, settings):
"""Update overlay display settings"""
self.overlay_settings.update(settings)
self.update()
def set_recording_indicator(self, recording):
"""Set recording status indicator"""
self.is_recording = recording
if recording:
self.recording_indicator.setText("🔴 REC")
self.recording_indicator.show()
else:
self.recording_indicator.hide()
def set_camera_status(self, status):
"""Set camera status (online/offline/error)"""
self.camera_status = status
status_colors = {
'online': '#27ae60', # Green
'offline': '#e74c3c', # Red
'error': '#f39c12', # Orange
'connecting': '#3498db' # Blue
}
color = status_colors.get(status, '#e74c3c')
self.status_label.setStyleSheet(f"color: {color}; font-size: 10pt;")
def take_snapshot(self):
"""Take a snapshot of current frame"""
self.snapshot_requested.emit()
# Visual feedback
self.setStyleSheet("border: 2px solid white;")
QTimer.singleShot(200, lambda: self.setStyleSheet(""))
def mouseDoubleClickEvent(self, event):
"""Handle double-click for fullscreen"""
if event.button() == Qt.LeftButton:
self.double_clicked.emit()
def paintEvent(self, event):
"""Custom paint event for video and overlays"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# Get drawing area
rect = self.video_frame.geometry()
# Draw video frame if available
if self.current_frame is not None:
# Scale frame to fit widget while maintaining aspect ratio
scaled_pixmap = self.current_frame.scaled(
rect.size(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
# Center the image
x = rect.x() + (rect.width() - scaled_pixmap.width()) // 2
y = rect.y() + (rect.height() - scaled_pixmap.height()) // 2
painter.drawPixmap(x, y, scaled_pixmap)
# Draw detection overlays
if self.detections and any(self.overlay_settings.values()):
self._draw_detections(painter, rect, scaled_pixmap.size())
else:
# Draw placeholder
painter.fillRect(rect, QColor(44, 62, 80))
painter.setPen(QColor(255, 255, 255))
painter.setFont(QFont("Arial", 12))
painter.drawText(rect, Qt.AlignCenter, "No Signal")
def _draw_detections(self, painter, display_rect, frame_size):
"""Draw detection overlays"""
# Calculate scaling factors
scale_x = frame_size.width() / display_rect.width()
scale_y = frame_size.height() / display_rect.height()
for detection in self.detections:
# Skip low confidence detections
confidence = detection.get('confidence', 0)
if confidence < self.confidence_threshold:
continue
# Get detection info
bbox = detection.get('bbox', [0, 0, 0, 0]) # [x, y, w, h]
class_name = detection.get('class', 'unknown')
track_id = detection.get('track_id', None)
speed = detection.get('speed', None)
# Scale bounding box to display coordinates
x = int(bbox[0] / scale_x) + display_rect.x()
y = int(bbox[1] / scale_y) + display_rect.y()
w = int(bbox[2] / scale_x)
h = int(bbox[3] / scale_y)
# Get color for this class
color = self.class_colors.get(class_name, self.class_colors['default'])
# Draw bounding box
if self.overlay_settings['show_boxes']:
pen = QPen(color, 2)
painter.setPen(pen)
painter.setBrush(Qt.NoBrush)
painter.drawRect(x, y, w, h)
# Draw labels
labels = []
if self.overlay_settings['show_confidence']:
labels.append(f"{class_name}: {confidence:.2f}")
if self.overlay_settings['show_tracks'] and track_id is not None:
labels.append(f"ID: {track_id}")
if self.overlay_settings['show_speed'] and speed is not None:
labels.append(f"{speed:.1f} km/h")
if labels:
self._draw_label(painter, x, y, labels, color)
def _draw_label(self, painter, x, y, labels, color):
"""Draw detection label with background"""
text = " | ".join(labels)
# Set font
font = QFont("Arial", 8, QFont.Bold)
painter.setFont(font)
# Calculate text size
fm = painter.fontMetrics()
text_rect = fm.boundingRect(text)
# Draw background
bg_rect = QRect(x, y - text_rect.height() - 4,
text_rect.width() + 8, text_rect.height() + 4)
bg_color = QColor(color)
bg_color.setAlpha(180)
painter.fillRect(bg_rect, bg_color)
# Draw text
painter.setPen(QColor(255, 255, 255))
painter.drawText(x + 4, y - 4, text)
def update_fps(self, fps):
"""Update FPS display"""
self.fps_label.setText(f"{fps:.1f} fps")