Files

422 lines
14 KiB
Python

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