316 lines
11 KiB
Python
316 lines
11 KiB
Python
"""
|
|
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")
|