""" 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}; }} """)