cleanup and files added
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
390
qt_app_pyside1/ui/widgets/alert_widget.py
Normal file
390
qt_app_pyside1/ui/widgets/alert_widget.py
Normal 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())
|
||||
362
qt_app_pyside1/ui/widgets/camera_control_panel.py
Normal file
362
qt_app_pyside1/ui/widgets/camera_control_panel.py
Normal 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;")
|
||||
171
qt_app_pyside1/ui/widgets/detection_overlay_widget.py
Normal file
171
qt_app_pyside1/ui/widgets/detection_overlay_widget.py
Normal 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
|
||||
280
qt_app_pyside1/ui/widgets/notification_center.py
Normal file
280
qt_app_pyside1/ui/widgets/notification_center.py
Normal 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
|
||||
290
qt_app_pyside1/ui/widgets/statistics_panel.py
Normal file
290
qt_app_pyside1/ui/widgets/statistics_panel.py
Normal 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")
|
||||
104
qt_app_pyside1/ui/widgets/status_indicator.py
Normal file
104
qt_app_pyside1/ui/widgets/status_indicator.py
Normal 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)
|
||||
315
qt_app_pyside1/ui/widgets/video_display_widget.py
Normal file
315
qt_app_pyside1/ui/widgets/video_display_widget.py
Normal 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")
|
||||
Reference in New Issue
Block a user