1479 lines
56 KiB
Python
1479 lines
56 KiB
Python
from PySide6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
|
QFrame, QComboBox, QCheckBox, QFileDialog, QProgressBar,
|
|
QGroupBox, QTextEdit
|
|
)
|
|
from PySide6.QtCore import Signal, Qt
|
|
from PySide6.QtGui import QPixmap, QFont, QDragEnterEvent, QDropEvent
|
|
import os
|
|
|
|
class VideoDropZone(QFrame):
|
|
"""Simple drag and drop zone for video files"""
|
|
video_dropped = Signal(str)
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.setupUI()
|
|
self.setAcceptDrops(True)
|
|
|
|
def setupUI(self):
|
|
self.setObjectName("VideoDropZone")
|
|
self.setStyleSheet("""
|
|
QFrame#VideoDropZone {
|
|
background: #1E1E1E;
|
|
border: 2px dashed #2C2C2C;
|
|
border-radius: 12px;
|
|
min-height: 200px;
|
|
}
|
|
QFrame#VideoDropZone:hover {
|
|
border: 2px dashed #007BFF;
|
|
background: #232323;
|
|
}
|
|
""")
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.setAlignment(Qt.AlignCenter)
|
|
layout.setSpacing(16)
|
|
|
|
# Drop icon
|
|
icon_label = QLabel("📹")
|
|
icon_label.setAlignment(Qt.AlignCenter)
|
|
icon_label.setStyleSheet("font-size: 48px; color: #B0B0B0;")
|
|
|
|
# Text
|
|
text_label = QLabel("Drop video file here\nor click to browse")
|
|
text_label.setAlignment(Qt.AlignCenter)
|
|
text_label.setStyleSheet("""
|
|
color: #B0B0B0;
|
|
font-size: 16px;
|
|
line-height: 1.4;
|
|
""")
|
|
|
|
# Browse button
|
|
self.browse_btn = QPushButton("Browse Files")
|
|
self.browse_btn.setObjectName("primary")
|
|
self.browse_btn.clicked.connect(self.browse_file)
|
|
|
|
layout.addWidget(icon_label)
|
|
layout.addWidget(text_label)
|
|
layout.addWidget(self.browse_btn)
|
|
|
|
def dragEnterEvent(self, event: QDragEnterEvent):
|
|
if event.mimeData().hasUrls():
|
|
urls = event.mimeData().urls()
|
|
if urls and urls[0].toLocalFile().lower().endswith(('.mp4', '.avi', '.mov', '.mkv', '.wmv')):
|
|
event.acceptProposedAction()
|
|
else:
|
|
event.ignore()
|
|
else:
|
|
event.ignore()
|
|
|
|
def dropEvent(self, event: QDropEvent):
|
|
urls = event.mimeData().urls()
|
|
if urls:
|
|
file_path = urls[0].toLocalFile()
|
|
if file_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv', '.wmv')):
|
|
self.video_dropped.emit(file_path)
|
|
event.acceptProposedAction()
|
|
else:
|
|
event.ignore()
|
|
|
|
def browse_file(self):
|
|
file_path, _ = QFileDialog.getOpenFileName(
|
|
self,
|
|
"Select Video File",
|
|
"",
|
|
"Video Files (*.mp4 *.avi *.mov *.mkv *.wmv);;All Files (*)"
|
|
)
|
|
if file_path:
|
|
self.video_dropped.emit(file_path)
|
|
|
|
class VideoAnalysisTab(QWidget):
|
|
"""Clean and simple video analysis tab"""
|
|
|
|
# Signals for data publishing
|
|
video_selected = Signal(str)
|
|
analysis_started = Signal()
|
|
analysis_stopped = Signal()
|
|
data_ready = Signal(dict) # For InfluxDB/MQTT/Grafana
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.current_video = None
|
|
self.setupUI()
|
|
|
|
def setupUI(self):
|
|
# Set dark background
|
|
self.setStyleSheet("""
|
|
QWidget {
|
|
background: #121212;
|
|
color: #FFFFFF;
|
|
}
|
|
|
|
QPushButton[objectName="primary"] {
|
|
background: #007BFF;
|
|
color: #FFFFFF;
|
|
border-radius: 8px;
|
|
font-weight: bold;
|
|
padding: 12px 24px;
|
|
font-size: 14px;
|
|
border: none;
|
|
min-width: 120px;
|
|
}
|
|
QPushButton[objectName="primary"]:hover {
|
|
background: #3399FF;
|
|
}
|
|
|
|
QPushButton[objectName="secondary"] {
|
|
background: #2ECC71;
|
|
color: #FFFFFF;
|
|
border-radius: 8px;
|
|
padding: 12px 24px;
|
|
font-size: 14px;
|
|
border: none;
|
|
min-width: 120px;
|
|
}
|
|
QPushButton[objectName="secondary"]:hover {
|
|
background: #48D187;
|
|
}
|
|
|
|
QPushButton[objectName="warning"] {
|
|
background: #E74C3C;
|
|
color: #FFFFFF;
|
|
border-radius: 8px;
|
|
font-weight: bold;
|
|
padding: 12px 24px;
|
|
font-size: 14px;
|
|
border: none;
|
|
min-width: 120px;
|
|
}
|
|
QPushButton[objectName="warning"]:hover {
|
|
background: #FF6B5A;
|
|
}
|
|
|
|
QGroupBox {
|
|
border: 1px solid #2C2C2C;
|
|
border-radius: 8px;
|
|
margin-top: 16px;
|
|
background: transparent;
|
|
font-weight: bold;
|
|
color: #FFFFFF;
|
|
font-size: 14px;
|
|
padding-top: 10px;
|
|
}
|
|
QGroupBox::title {
|
|
subcontrol-origin: margin;
|
|
left: 12px;
|
|
top: 0px;
|
|
padding: 0 8px;
|
|
background: #121212;
|
|
}
|
|
""")
|
|
|
|
# Main layout
|
|
main_layout = QHBoxLayout(self)
|
|
main_layout.setContentsMargins(20, 20, 20, 20)
|
|
main_layout.setSpacing(20)
|
|
|
|
# Left side - Video input and display
|
|
left_widget = QWidget()
|
|
left_layout = QVBoxLayout(left_widget)
|
|
left_layout.setSpacing(20)
|
|
|
|
# Video Input Section
|
|
input_group = QGroupBox("📹 Video Input")
|
|
input_layout = QVBoxLayout(input_group)
|
|
input_layout.setSpacing(16)
|
|
|
|
# Drop zone
|
|
self.drop_zone = VideoDropZone()
|
|
self.drop_zone.video_dropped.connect(self.load_video)
|
|
|
|
# Current video info
|
|
self.video_info = QLabel("No video selected")
|
|
self.video_info.setStyleSheet("""
|
|
color: #B0B0B0;
|
|
font-size: 12px;
|
|
padding: 8px 12px;
|
|
background: #1E1E1E;
|
|
border-radius: 6px;
|
|
""")
|
|
|
|
input_layout.addWidget(self.drop_zone)
|
|
input_layout.addWidget(self.video_info)
|
|
|
|
# Video Display Section
|
|
display_group = QGroupBox("🎬 Video Preview")
|
|
display_layout = QVBoxLayout(display_group)
|
|
|
|
# Video preview
|
|
self.video_preview = QLabel("Video preview will appear here")
|
|
self.video_preview.setAlignment(Qt.AlignCenter)
|
|
self.video_preview.setMinimumHeight(300)
|
|
self.video_preview.setStyleSheet("""
|
|
QLabel {
|
|
background: #1E1E1E;
|
|
border: 1px solid #2C2C2C;
|
|
border-radius: 8px;
|
|
color: #B0B0B0;
|
|
font-size: 16px;
|
|
}
|
|
""")
|
|
|
|
# Progress bar
|
|
self.progress_bar = QProgressBar()
|
|
self.progress_bar.setVisible(False)
|
|
self.progress_bar.setStyleSheet("""
|
|
QProgressBar {
|
|
border: 1px solid #2C2C2C;
|
|
border-radius: 4px;
|
|
background: #1E1E1E;
|
|
text-align: center;
|
|
color: #FFFFFF;
|
|
font-size: 12px;
|
|
}
|
|
QProgressBar::chunk {
|
|
background: #007BFF;
|
|
border-radius: 3px;
|
|
}
|
|
""")
|
|
|
|
display_layout.addWidget(self.video_preview)
|
|
display_layout.addWidget(self.progress_bar)
|
|
|
|
left_layout.addWidget(input_group)
|
|
left_layout.addWidget(display_group)
|
|
|
|
# Right side - Simple controls and data output
|
|
right_widget = QWidget()
|
|
right_widget.setFixedWidth(320)
|
|
right_layout = QVBoxLayout(right_widget)
|
|
right_layout.setSpacing(20)
|
|
|
|
# Detection Settings
|
|
detection_group = QGroupBox("🎯 Detection Settings")
|
|
detection_layout = QVBoxLayout(detection_group)
|
|
detection_layout.setSpacing(12)
|
|
|
|
self.detect_vehicles = QCheckBox("🚗 Detect Vehicles")
|
|
self.detect_vehicles.setChecked(True)
|
|
self.detect_vehicles.setStyleSheet("color: #FFFFFF; font-size: 12px;")
|
|
|
|
self.detect_pedestrians = QCheckBox("🚶 Detect Pedestrians")
|
|
self.detect_pedestrians.setChecked(True)
|
|
self.detect_pedestrians.setStyleSheet("color: #FFFFFF; font-size: 12px;")
|
|
|
|
self.detect_traffic_lights = QCheckBox("🚦 Detect Traffic Lights")
|
|
self.detect_traffic_lights.setChecked(True)
|
|
self.detect_traffic_lights.setStyleSheet("color: #FFFFFF; font-size: 12px;")
|
|
|
|
detection_layout.addWidget(self.detect_vehicles)
|
|
detection_layout.addWidget(self.detect_pedestrians)
|
|
detection_layout.addWidget(self.detect_traffic_lights)
|
|
|
|
# Control Buttons
|
|
controls_group = QGroupBox("🎮 Controls")
|
|
controls_layout = QVBoxLayout(controls_group)
|
|
controls_layout.setSpacing(12)
|
|
|
|
self.start_btn = QPushButton("▶️ Start Analysis")
|
|
self.start_btn.setObjectName("primary")
|
|
self.start_btn.clicked.connect(self.start_analysis)
|
|
self.start_btn.setEnabled(False)
|
|
|
|
self.stop_btn = QPushButton("⏹️ Stop Analysis")
|
|
self.stop_btn.setObjectName("warning")
|
|
self.stop_btn.clicked.connect(self.stop_analysis)
|
|
self.stop_btn.setEnabled(False)
|
|
|
|
# Data Publishing Button (Main feature for InfluxDB/MQTT/Grafana)
|
|
self.publish_data_btn = QPushButton("📊 Publish to Grafana")
|
|
self.publish_data_btn.setObjectName("secondary")
|
|
self.publish_data_btn.clicked.connect(self.publish_data)
|
|
self.publish_data_btn.setEnabled(False)
|
|
|
|
controls_layout.addWidget(self.start_btn)
|
|
controls_layout.addWidget(self.stop_btn)
|
|
controls_layout.addWidget(self.publish_data_btn)
|
|
|
|
# Live Results
|
|
results_group = QGroupBox("📈 Live Results")
|
|
results_layout = QVBoxLayout(results_group)
|
|
results_layout.setSpacing(8)
|
|
|
|
self.total_vehicles = QLabel("🚗 Vehicles: 0")
|
|
self.total_vehicles.setStyleSheet("color: #00FFFF; font-size: 12px;")
|
|
|
|
self.total_pedestrians = QLabel("🚶 Pedestrians: 0")
|
|
self.total_pedestrians.setStyleSheet("color: #FFA500; font-size: 12px;")
|
|
|
|
self.traffic_lights = QLabel("🚦 Traffic Lights: 0")
|
|
self.traffic_lights.setStyleSheet("color: #FFD700; font-size: 12px;")
|
|
|
|
self.avg_speed = QLabel("⚡ Avg Speed: 0 km/h")
|
|
self.avg_speed.setStyleSheet("color: #00FF00; font-size: 12px;")
|
|
|
|
results_layout.addWidget(self.total_vehicles)
|
|
results_layout.addWidget(self.total_pedestrians)
|
|
results_layout.addWidget(self.traffic_lights)
|
|
results_layout.addWidget(self.avg_speed)
|
|
|
|
# Data Output Log
|
|
log_group = QGroupBox("📋 Data Log")
|
|
log_layout = QVBoxLayout(log_group)
|
|
|
|
self.data_log = QTextEdit()
|
|
self.data_log.setMaximumHeight(150)
|
|
self.data_log.setStyleSheet("""
|
|
QTextEdit {
|
|
background: #1E1E1E;
|
|
border: 1px solid #2C2C2C;
|
|
border-radius: 6px;
|
|
color: #00E6E6;
|
|
font-family: 'Consolas', monospace;
|
|
font-size: 10px;
|
|
padding: 8px;
|
|
}
|
|
""")
|
|
self.data_log.setPlainText("📊 Data publishing log will appear here...")
|
|
|
|
log_layout.addWidget(self.data_log)
|
|
|
|
right_layout.addWidget(detection_group)
|
|
right_layout.addWidget(controls_group)
|
|
right_layout.addWidget(results_group)
|
|
right_layout.addWidget(log_group)
|
|
right_layout.addStretch()
|
|
|
|
# Add to main layout
|
|
main_layout.addWidget(left_widget, 2)
|
|
main_layout.addWidget(right_widget, 1)
|
|
|
|
def load_video(self, video_path):
|
|
"""Load video file"""
|
|
if os.path.exists(video_path):
|
|
self.current_video = video_path
|
|
filename = os.path.basename(video_path)
|
|
|
|
# Update video info
|
|
self.video_info.setText(f"📹 {filename}")
|
|
self.video_info.setStyleSheet("""
|
|
color: #00E6E6;
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
padding: 8px 12px;
|
|
background: #1E1E1E;
|
|
border: 1px solid #00E6E6;
|
|
border-radius: 6px;
|
|
""")
|
|
|
|
# Enable start button
|
|
self.start_btn.setEnabled(True)
|
|
|
|
# Update preview
|
|
self.video_preview.setText(f"✅ Video loaded: {filename}\nReady for analysis")
|
|
self.video_preview.setStyleSheet("""
|
|
QLabel {
|
|
background: #1E1E1E;
|
|
border: 1px solid #00E6E6;
|
|
border-radius: 8px;
|
|
color: #00E6E6;
|
|
font-size: 14px;
|
|
}
|
|
""")
|
|
|
|
self.video_selected.emit(video_path)
|
|
self.log_data(f"📹 Video loaded: {filename}")
|
|
|
|
def start_analysis(self):
|
|
"""Start video analysis"""
|
|
if self.current_video:
|
|
self.start_btn.setEnabled(False)
|
|
self.stop_btn.setEnabled(True)
|
|
self.progress_bar.setVisible(True)
|
|
self.progress_bar.setValue(0)
|
|
|
|
self.video_preview.setText("🔄 Analysis in progress...\nProcessing video frames")
|
|
self.video_preview.setStyleSheet("""
|
|
QLabel {
|
|
background: #1E1E1E;
|
|
border: 1px solid #FFD700;
|
|
border-radius: 8px;
|
|
color: #FFD700;
|
|
font-size: 14px;
|
|
}
|
|
""")
|
|
|
|
self.analysis_started.emit()
|
|
self.log_data("▶️ Analysis started")
|
|
|
|
def stop_analysis(self):
|
|
"""Stop video analysis"""
|
|
self.start_btn.setEnabled(True)
|
|
self.stop_btn.setEnabled(False)
|
|
self.publish_data_btn.setEnabled(True)
|
|
self.progress_bar.setVisible(False)
|
|
|
|
self.video_preview.setText("⏹️ Analysis completed\nData ready for publishing")
|
|
self.video_preview.setStyleSheet("""
|
|
QLabel {
|
|
background: #1E1E1E;
|
|
border: 1px solid #2ECC71;
|
|
border-radius: 8px;
|
|
color: #2ECC71;
|
|
font-size: 14px;
|
|
}
|
|
""")
|
|
|
|
self.analysis_stopped.emit()
|
|
self.log_data("⏹️ Analysis completed")
|
|
|
|
def publish_data(self):
|
|
"""Publish data to InfluxDB/MQTT/Grafana"""
|
|
# Prepare data for publishing
|
|
data = {
|
|
'timestamp': 'now',
|
|
'video_file': os.path.basename(self.current_video) if self.current_video else '',
|
|
'total_vehicles': int(self.total_vehicles.text().split(': ')[1]),
|
|
'total_pedestrians': int(self.total_pedestrians.text().split(': ')[1]),
|
|
'traffic_lights': int(self.traffic_lights.text().split(': ')[1]),
|
|
'avg_speed': float(self.avg_speed.text().split(': ')[1].split(' ')[0]),
|
|
'detection_settings': {
|
|
'vehicles': self.detect_vehicles.isChecked(),
|
|
'pedestrians': self.detect_pedestrians.isChecked(),
|
|
'traffic_lights': self.detect_traffic_lights.isChecked()
|
|
}
|
|
}
|
|
|
|
# Emit data for external handling
|
|
self.data_ready.emit(data)
|
|
|
|
# Update UI
|
|
self.log_data("📊 Data published to InfluxDB/MQTT/Grafana")
|
|
self.log_data(f"📈 Vehicles: {data['total_vehicles']}, Pedestrians: {data['total_pedestrians']}")
|
|
|
|
# Show success
|
|
self.video_preview.setText("📊 Data published successfully!\nCheck Grafana dashboard")
|
|
self.video_preview.setStyleSheet("""
|
|
QLabel {
|
|
background: #1E1E1E;
|
|
border: 1px solid #00FF00;
|
|
border-radius: 8px;
|
|
color: #00FF00;
|
|
font-size: 14px;
|
|
}
|
|
""")
|
|
|
|
def update_progress(self, value):
|
|
"""Update progress bar"""
|
|
self.progress_bar.setValue(value)
|
|
|
|
def update_results(self, vehicles=0, pedestrians=0, traffic_lights=0, speed=0.0):
|
|
"""Update analysis results"""
|
|
self.total_vehicles.setText(f"🚗 Vehicles: {vehicles}")
|
|
self.total_pedestrians.setText(f"🚶 Pedestrians: {pedestrians}")
|
|
self.traffic_lights.setText(f"🚦 Traffic Lights: {traffic_lights}")
|
|
self.avg_speed.setText(f"⚡ Avg Speed: {speed:.1f} km/h")
|
|
|
|
def log_data(self, message):
|
|
"""Add message to data log"""
|
|
import datetime
|
|
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
|
self.data_log.append(f"[{timestamp}] {message}")
|
|
|
|
# Keep log reasonable size
|
|
if self.data_log.document().blockCount() > 50:
|
|
cursor = self.data_log.textCursor()
|
|
cursor.movePosition(cursor.Start)
|
|
cursor.select(cursor.BlockUnderCursor)
|
|
cursor.removeSelectedText()
|
|
cursor.deleteChar() # Remove the newline
|
|
"""Advanced overlay for Smart Intersection analytics."""
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setStyleSheet("""
|
|
background: rgba(0,20,40,0.85);
|
|
border: 2px solid #03DAC5;
|
|
border-radius: 12px;
|
|
color: #fff;
|
|
font-family: 'Consolas', 'SF Mono', 'monospace';
|
|
font-size: 12px;
|
|
""")
|
|
self.setFixedHeight(140)
|
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
self.setAttribute(Qt.WA_TransparentForMouseEvents)
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(16, 12, 16, 12)
|
|
layout.setSpacing(4)
|
|
|
|
# Title
|
|
title = QLabel("🚦 Smart Intersection Analytics")
|
|
title.setStyleSheet("color: #03DAC5; font-weight: bold; font-size: 14px;")
|
|
layout.addWidget(title)
|
|
|
|
# Scene data
|
|
self.scene_label = QLabel("Scene: Multi-Camera Fusion")
|
|
self.tracking_label = QLabel("Active Tracks: 0")
|
|
self.roi_label = QLabel("ROI Events: 0")
|
|
|
|
# Camera data
|
|
self.camera_label = QLabel("Cameras: North(0) East(0) South(0) West(0)")
|
|
|
|
# Analytics data
|
|
self.analytics_label = QLabel("Analytics: Crosswalk(0) Lane(0) Safety(0)")
|
|
|
|
for w in [self.scene_label, self.tracking_label, self.roi_label,
|
|
self.camera_label, self.analytics_label]:
|
|
w.setStyleSheet("color: #fff;")
|
|
layout.addWidget(w)
|
|
|
|
def update_smart_intersection(self, scene_data):
|
|
"""Update smart intersection specific data"""
|
|
if not scene_data:
|
|
return
|
|
|
|
# Update tracking info
|
|
active_tracks = scene_data.get('active_tracks', 0)
|
|
self.tracking_label.setText(f"Active Tracks: {active_tracks}")
|
|
|
|
# Update ROI events
|
|
roi_events = scene_data.get('roi_events', 0)
|
|
self.roi_label.setText(f"ROI Events: {roi_events}")
|
|
|
|
# Update camera data
|
|
cameras = scene_data.get('cameras', {})
|
|
north = cameras.get('north', 0)
|
|
east = cameras.get('east', 0)
|
|
south = cameras.get('south', 0)
|
|
west = cameras.get('west', 0)
|
|
self.camera_label.setText(f"Cameras: North({north}) East({east}) South({south}) West({west})")
|
|
|
|
# Update analytics
|
|
analytics = scene_data.get('analytics', {})
|
|
crosswalk = analytics.get('crosswalk_events', 0)
|
|
lane = analytics.get('lane_events', 0)
|
|
safety = analytics.get('safety_events', 0)
|
|
self.analytics_label.setText(f"Analytics: Crosswalk({crosswalk}) Lane({lane}) Safety({safety})")
|
|
|
|
|
|
class IntersectionROIWidget(QFrame):
|
|
"""Widget for defining and managing ROI regions for smart intersection"""
|
|
roi_updated = Signal(dict)
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setStyleSheet("""
|
|
QFrame {
|
|
background: #1a1a1a;
|
|
border: 1px solid #424242;
|
|
border-radius: 8px;
|
|
}
|
|
""")
|
|
self.setFixedWidth(300)
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(16, 16, 16, 16)
|
|
|
|
# Title
|
|
title = QLabel("🎯 Region of Interest (ROI)")
|
|
title.setStyleSheet("color: #03DAC5; font-weight: bold; font-size: 14px;")
|
|
layout.addWidget(title)
|
|
|
|
# ROI Type selection
|
|
type_layout = QHBoxLayout()
|
|
type_layout.addWidget(QLabel("Type:"))
|
|
self.roi_type = QComboBox()
|
|
self.roi_type.addItems(["Crosswalk", "Traffic Lane", "Safety Zone", "Intersection Center"])
|
|
type_layout.addWidget(self.roi_type)
|
|
layout.addLayout(type_layout)
|
|
|
|
# ROI List
|
|
self.roi_list = QListWidget()
|
|
self.roi_list.setMaximumHeight(120)
|
|
layout.addWidget(self.roi_list)
|
|
|
|
# ROI Controls
|
|
roi_controls = QHBoxLayout()
|
|
self.add_roi_btn = QPushButton("Add ROI")
|
|
self.delete_roi_btn = QPushButton("Delete")
|
|
self.add_roi_btn.setStyleSheet("background: #27ae60; color: white; border-radius: 4px; padding: 6px;")
|
|
self.delete_roi_btn.setStyleSheet("background: #e74c3c; color: white; border-radius: 4px; padding: 6px;")
|
|
roi_controls.addWidget(self.add_roi_btn)
|
|
roi_controls.addWidget(self.delete_roi_btn)
|
|
layout.addLayout(roi_controls)
|
|
|
|
# Analytics settings
|
|
analytics_group = QGroupBox("Analytics Settings")
|
|
analytics_layout = QVBoxLayout(analytics_group)
|
|
|
|
self.enable_tracking = QCheckBox("Multi-Object Tracking")
|
|
self.enable_speed = QCheckBox("Speed Estimation")
|
|
self.enable_direction = QCheckBox("Direction Analysis")
|
|
self.enable_safety = QCheckBox("Safety Monitoring")
|
|
|
|
for cb in [self.enable_tracking, self.enable_speed, self.enable_direction, self.enable_safety]:
|
|
cb.setChecked(True)
|
|
cb.setStyleSheet("color: white;")
|
|
analytics_layout.addWidget(cb)
|
|
|
|
layout.addWidget(analytics_group)
|
|
|
|
# Connect signals
|
|
self.add_roi_btn.clicked.connect(self._add_roi)
|
|
self.delete_roi_btn.clicked.connect(self._delete_roi)
|
|
|
|
# Initialize with default ROIs
|
|
self._init_default_rois()
|
|
|
|
def _init_default_rois(self):
|
|
"""Initialize with default intersection ROIs"""
|
|
default_rois = [
|
|
"North Crosswalk",
|
|
"South Crosswalk",
|
|
"East Crosswalk",
|
|
"West Crosswalk",
|
|
"Center Intersection",
|
|
"North Lane",
|
|
"South Lane",
|
|
"East Lane",
|
|
"West Lane"
|
|
]
|
|
|
|
for roi in default_rois:
|
|
item = QListWidgetItem(roi)
|
|
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
|
item.setCheckState(Qt.Checked)
|
|
self.roi_list.addItem(item)
|
|
|
|
def _add_roi(self):
|
|
"""Add new ROI"""
|
|
roi_type = self.roi_type.currentText()
|
|
roi_name = f"{roi_type}_{self.roi_list.count() + 1}"
|
|
|
|
item = QListWidgetItem(roi_name)
|
|
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
|
item.setCheckState(Qt.Checked)
|
|
self.roi_list.addItem(item)
|
|
|
|
self._emit_roi_update()
|
|
|
|
def _delete_roi(self):
|
|
"""Delete selected ROI"""
|
|
current_row = self.roi_list.currentRow()
|
|
if current_row >= 0:
|
|
self.roi_list.takeItem(current_row)
|
|
self._emit_roi_update()
|
|
|
|
def _emit_roi_update(self):
|
|
"""Emit ROI configuration update"""
|
|
roi_config = {
|
|
'rois': [],
|
|
'analytics': {
|
|
'tracking': self.enable_tracking.isChecked(),
|
|
'speed': self.enable_speed.isChecked(),
|
|
'direction': self.enable_direction.isChecked(),
|
|
'safety': self.enable_safety.isChecked()
|
|
}
|
|
}
|
|
|
|
for i in range(self.roi_list.count()):
|
|
item = self.roi_list.item(i)
|
|
roi_config['rois'].append({
|
|
'name': item.text(),
|
|
'enabled': item.checkState() == Qt.Checked
|
|
})
|
|
|
|
self.roi_updated.emit(roi_config)
|
|
|
|
|
|
class MultiCameraView(QFrame):
|
|
"""Multi-camera view for smart intersection"""
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setStyleSheet("""
|
|
QFrame {
|
|
background: #0a0a0a;
|
|
border: 2px solid #424242;
|
|
border-radius: 8px;
|
|
}
|
|
""")
|
|
|
|
layout = QGridLayout(self)
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
layout.setSpacing(4)
|
|
|
|
# Create camera views
|
|
self.camera_views = {}
|
|
positions = [('North', 0, 1), ('West', 1, 0), ('East', 1, 2), ('South', 2, 1)]
|
|
|
|
for pos_name, row, col in positions:
|
|
view = self._create_camera_view(pos_name)
|
|
self.camera_views[pos_name.lower()] = view
|
|
layout.addWidget(view, row, col)
|
|
|
|
# Center intersection view
|
|
center_view = self._create_intersection_center()
|
|
layout.addWidget(center_view, 1, 1)
|
|
|
|
def _create_camera_view(self, position):
|
|
"""Create individual camera view"""
|
|
view = QFrame()
|
|
view.setStyleSheet("""
|
|
background: #1a1a1a;
|
|
border: 1px solid #555;
|
|
border-radius: 4px;
|
|
""")
|
|
view.setMinimumSize(160, 120)
|
|
view.setMaximumSize(200, 150)
|
|
|
|
layout = QVBoxLayout(view)
|
|
layout.setContentsMargins(4, 4, 4, 4)
|
|
|
|
# Title
|
|
title = QLabel(f"📹 {position}")
|
|
title.setStyleSheet("color: #03DAC5; font-weight: bold; font-size: 10px;")
|
|
title.setAlignment(Qt.AlignCenter)
|
|
layout.addWidget(title)
|
|
|
|
# Video area
|
|
video_area = QLabel("No feed")
|
|
video_area.setStyleSheet("background: #000; color: #666; border: 1px dashed #333;")
|
|
video_area.setAlignment(Qt.AlignCenter)
|
|
video_area.setMinimumHeight(80)
|
|
layout.addWidget(video_area)
|
|
|
|
# Stats
|
|
stats = QLabel("Objects: 0")
|
|
stats.setStyleSheet("color: #aaa; font-size: 9px;")
|
|
stats.setAlignment(Qt.AlignCenter)
|
|
layout.addWidget(stats)
|
|
|
|
return view
|
|
|
|
def _create_intersection_center(self):
|
|
"""Create center intersection overview"""
|
|
view = QFrame()
|
|
view.setStyleSheet("""
|
|
background: #2a1a1a;
|
|
border: 2px solid #03DAC5;
|
|
border-radius: 8px;
|
|
""")
|
|
view.setMinimumSize(160, 120)
|
|
view.setMaximumSize(200, 150)
|
|
|
|
layout = QVBoxLayout(view)
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
title = QLabel("🚦 Intersection")
|
|
title.setStyleSheet("color: #03DAC5; font-weight: bold; font-size: 12px;")
|
|
title.setAlignment(Qt.AlignCenter)
|
|
layout.addWidget(title)
|
|
|
|
# Intersection map
|
|
map_area = QLabel("Scene Map")
|
|
map_area.setStyleSheet("background: #000; color: #03DAC5; border: 1px solid #03DAC5;")
|
|
map_area.setAlignment(Qt.AlignCenter)
|
|
map_area.setMinimumHeight(80)
|
|
layout.addWidget(map_area)
|
|
|
|
# Total stats
|
|
total_stats = QLabel("Total Objects: 0")
|
|
total_stats.setStyleSheet("color: #03DAC5; font-size: 10px; font-weight: bold;")
|
|
total_stats.setAlignment(Qt.AlignCenter)
|
|
layout.addWidget(total_stats)
|
|
|
|
return view
|
|
|
|
def update_camera_feed(self, camera_position, pixmap, object_count=0):
|
|
"""Update specific camera feed"""
|
|
if camera_position.lower() in self.camera_views:
|
|
view = self.camera_views[camera_position.lower()]
|
|
video_label = view.findChild(QLabel)
|
|
if video_label and pixmap:
|
|
scaled = pixmap.scaled(video_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
|
video_label.setPixmap(scaled)
|
|
|
|
# Update stats
|
|
stats_labels = view.findChildren(QLabel)
|
|
if len(stats_labels) >= 3: # title, video, stats
|
|
stats_labels[2].setText(f"Objects: {object_count}")
|
|
|
|
|
|
class DiagnosticOverlay(QFrame):
|
|
"""Semi-transparent overlay for diagnostics."""
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setStyleSheet("""
|
|
background: rgba(0,0,0,0.5);
|
|
border-radius: 8px;
|
|
color: #fff;
|
|
font-family: 'Consolas', 'SF Mono', 'monospace';
|
|
font-size: 13px;
|
|
""")
|
|
# self.setFixedWidth(260) # Remove fixed width
|
|
self.setFixedHeight(90)
|
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) # Allow horizontal stretch
|
|
self.setAttribute(Qt.WA_TransparentForMouseEvents)
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(12, 8, 12, 8)
|
|
self.model_label = QLabel("Model: -")
|
|
self.device_label = QLabel("Device: -")
|
|
self.stats_label = QLabel("Cars: 0 | Trucks: 0 | Ped: 0 | TLights: 0 | Moto: 0")
|
|
for w in [self.model_label, self.device_label, self.stats_label]:
|
|
w.setStyleSheet("color: #fff;")
|
|
layout.addWidget(w)
|
|
layout.addStretch(1)
|
|
|
|
def update_overlay(self, model, device, cars, trucks, peds, tlights, motorcycles):
|
|
self.model_label.setText(f"Model: {model}")
|
|
self.device_label.setText(f"Device: {device}")
|
|
self.stats_label.setText(f"Cars: {cars} | Trucks: {trucks} | Ped: {peds} | TLights: {tlights} | Moto: {motorcycles}")
|
|
|
|
class VideoDetectionTab(QWidget):
|
|
file_selected = Signal(str)
|
|
play_clicked = Signal()
|
|
pause_clicked = Signal()
|
|
stop_clicked = Signal()
|
|
detection_toggled = Signal(bool)
|
|
screenshot_clicked = Signal()
|
|
seek_changed = Signal(int)
|
|
auto_select_model_device = Signal()
|
|
|
|
# Smart Intersection signals
|
|
smart_intersection_enabled = Signal(bool)
|
|
multi_camera_mode_enabled = Signal(bool)
|
|
roi_configuration_changed = Signal(dict)
|
|
scene_analytics_toggled = Signal(bool)
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.video_loaded = False
|
|
self.smart_intersection_mode = False
|
|
self.multi_camera_mode = False
|
|
|
|
# Load smart intersection config
|
|
self.load_smart_intersection_config()
|
|
|
|
# Main layout
|
|
main_layout = QHBoxLayout(self)
|
|
main_layout.setContentsMargins(16, 16, 16, 16)
|
|
main_layout.setSpacing(16)
|
|
|
|
# Left panel - video and controls
|
|
left_panel = self._create_left_panel()
|
|
main_layout.addWidget(left_panel, 3) # 3/4 of the space
|
|
|
|
# Right panel - smart intersection controls
|
|
right_panel = self._create_right_panel()
|
|
main_layout.addWidget(right_panel, 1) # 1/4 of the space
|
|
|
|
def load_smart_intersection_config(self):
|
|
"""Load smart intersection configuration"""
|
|
config_path = Path(__file__).parent.parent / "config" / "smart-intersection" / "desktop-config.json"
|
|
try:
|
|
if config_path.exists():
|
|
with open(config_path, 'r') as f:
|
|
self.smart_config = json.load(f)
|
|
else:
|
|
self.smart_config = self._get_default_config()
|
|
except Exception as e:
|
|
print(f"Error loading smart intersection config: {e}")
|
|
self.smart_config = self._get_default_config()
|
|
|
|
def _get_default_config(self):
|
|
"""Get default smart intersection configuration"""
|
|
return {
|
|
"desktop_app_config": {
|
|
"scene_analytics": {
|
|
"enable_multi_camera": True,
|
|
"enable_roi_analytics": True,
|
|
"enable_vlm_integration": True
|
|
},
|
|
"camera_settings": {
|
|
"max_cameras": 4,
|
|
"default_fps": 30
|
|
},
|
|
"analytics_settings": {
|
|
"object_tracking": True,
|
|
"speed_estimation": True,
|
|
"direction_analysis": True,
|
|
"safety_monitoring": True
|
|
}
|
|
}
|
|
}
|
|
|
|
def _create_left_panel(self):
|
|
"""Create main video panel"""
|
|
panel = QWidget()
|
|
layout = QVBoxLayout(panel)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(16)
|
|
|
|
# Smart Intersection Mode Toggle
|
|
mode_bar = self._create_mode_bar()
|
|
layout.addWidget(mode_bar)
|
|
|
|
# File select bar
|
|
file_bar = self._create_file_bar()
|
|
layout.addWidget(file_bar)
|
|
|
|
# Video display area (with tabs for different modes)
|
|
self.video_tabs = QTabWidget()
|
|
self.video_tabs.setStyleSheet("""
|
|
QTabWidget::pane {
|
|
border: 1px solid #424242;
|
|
background: #121212;
|
|
border-radius: 8px;
|
|
}
|
|
QTabBar::tab {
|
|
background: #232323;
|
|
color: #fff;
|
|
padding: 8px 16px;
|
|
margin-right: 2px;
|
|
border-top-left-radius: 8px;
|
|
border-top-right-radius: 8px;
|
|
}
|
|
QTabBar::tab:selected {
|
|
background: #03DAC5;
|
|
color: #000;
|
|
}
|
|
""")
|
|
|
|
# Single camera tab
|
|
self.single_cam_widget = self._create_single_camera_view()
|
|
self.video_tabs.addTab(self.single_cam_widget, "📹 Single Camera")
|
|
|
|
# Multi-camera tab
|
|
self.multi_cam_widget = MultiCameraView()
|
|
self.video_tabs.addTab(self.multi_cam_widget, "🚦 Multi-Camera Intersection")
|
|
|
|
layout.addWidget(self.video_tabs)
|
|
|
|
# Analytics overlay
|
|
self.analytics_overlay = self._create_analytics_overlay()
|
|
layout.addWidget(self.analytics_overlay)
|
|
|
|
# Control bar
|
|
control_bar = self._create_control_bar()
|
|
layout.addWidget(control_bar)
|
|
|
|
return panel
|
|
|
|
def _create_mode_bar(self):
|
|
"""Create smart intersection mode toggle bar"""
|
|
bar = QFrame()
|
|
bar.setStyleSheet("""
|
|
QFrame {
|
|
background: #1a2332;
|
|
border: 2px solid #03DAC5;
|
|
border-radius: 12px;
|
|
padding: 8px;
|
|
}
|
|
""")
|
|
bar.setFixedHeight(60)
|
|
|
|
layout = QHBoxLayout(bar)
|
|
layout.setContentsMargins(16, 8, 16, 8)
|
|
|
|
# Smart Intersection Toggle
|
|
self.smart_intersection_toggle = QCheckBox("🚦 Smart Intersection Mode")
|
|
self.smart_intersection_toggle.setStyleSheet("""
|
|
QCheckBox {
|
|
color: #03DAC5;
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
}
|
|
QCheckBox::indicator {
|
|
width: 20px;
|
|
height: 20px;
|
|
}
|
|
QCheckBox::indicator:checked {
|
|
background: #03DAC5;
|
|
border: 2px solid #03DAC5;
|
|
border-radius: 4px;
|
|
}
|
|
""")
|
|
self.smart_intersection_toggle.toggled.connect(self._toggle_smart_intersection)
|
|
layout.addWidget(self.smart_intersection_toggle)
|
|
|
|
layout.addSpacing(32)
|
|
|
|
# Multi-camera Toggle
|
|
self.multi_camera_toggle = QCheckBox("📹 Multi-Camera Fusion")
|
|
self.multi_camera_toggle.setStyleSheet("""
|
|
QCheckBox {
|
|
color: #e67e22;
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
}
|
|
QCheckBox::indicator {
|
|
width: 20px;
|
|
height: 20px;
|
|
}
|
|
QCheckBox::indicator:checked {
|
|
background: #e67e22;
|
|
border: 2px solid #e67e22;
|
|
border-radius: 4px;
|
|
}
|
|
""")
|
|
self.multi_camera_toggle.toggled.connect(self._toggle_multi_camera)
|
|
layout.addWidget(self.multi_camera_toggle)
|
|
|
|
layout.addStretch()
|
|
|
|
# Status indicator
|
|
self.mode_status = QLabel("Standard Detection Mode")
|
|
self.mode_status.setStyleSheet("color: #bbb; font-size: 12px;")
|
|
layout.addWidget(self.mode_status)
|
|
|
|
return bar
|
|
|
|
def _create_file_bar(self):
|
|
"""Create file selection bar"""
|
|
widget = QWidget()
|
|
bar = QHBoxLayout(widget)
|
|
|
|
self.file_btn = QPushButton()
|
|
self.file_btn.setIcon(QIcon.fromTheme("folder-video"))
|
|
self.file_btn.setText("Select Video")
|
|
self.file_btn.setStyleSheet("padding: 8px 18px; border-radius: 8px; background: #232323; color: #fff;")
|
|
self.file_label = QLabel("No file selected")
|
|
self.file_label.setStyleSheet("color: #bbb; font-size: 13px;")
|
|
self.file_btn.clicked.connect(self._select_file)
|
|
|
|
bar.addWidget(self.file_btn)
|
|
bar.addWidget(self.file_label)
|
|
bar.addStretch()
|
|
|
|
return widget
|
|
|
|
def _create_single_camera_view(self):
|
|
"""Create single camera view widget"""
|
|
widget = QWidget()
|
|
layout = QVBoxLayout(widget)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# Video frame
|
|
video_frame = QFrame()
|
|
video_frame.setStyleSheet("""
|
|
background: #121212;
|
|
border: 1px solid #424242;
|
|
border-radius: 8px;
|
|
""")
|
|
video_frame.setMinimumSize(640, 360)
|
|
video_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
|
|
video_layout = QVBoxLayout(video_frame)
|
|
video_layout.setContentsMargins(0, 0, 0, 0)
|
|
video_layout.setAlignment(Qt.AlignCenter)
|
|
|
|
self.video_label = QLabel()
|
|
self.video_label.setAlignment(Qt.AlignCenter)
|
|
self.video_label.setStyleSheet("background: transparent; color: #888; font-size: 18px;")
|
|
self.video_label.setText("No video loaded. Please select a file.")
|
|
self.video_label.setMinimumSize(640, 360)
|
|
self.video_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
video_layout.addWidget(self.video_label)
|
|
|
|
layout.addWidget(video_frame)
|
|
return widget
|
|
|
|
def _create_analytics_overlay(self):
|
|
"""Create analytics overlay that switches based on mode"""
|
|
container = QWidget()
|
|
self.overlay_layout = QVBoxLayout(container)
|
|
self.overlay_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# Standard overlay
|
|
self.standard_overlay = DiagnosticOverlay()
|
|
self.standard_overlay.setStyleSheet(self.standard_overlay.styleSheet() + "border: 1px solid #03DAC5;")
|
|
|
|
# Smart intersection overlay
|
|
self.smart_overlay = SmartIntersectionOverlay()
|
|
|
|
# Badge bar
|
|
self.badge_bar = QHBoxLayout()
|
|
self.badge_bar.setContentsMargins(0, 8, 0, 8)
|
|
|
|
self.fps_badge = QLabel("FPS: --")
|
|
self.fps_badge.setStyleSheet("background: #27ae60; color: #fff; border-radius: 12px; padding: 4px 24px; font-weight: bold; font-size: 15px;")
|
|
self.fps_badge.setAlignment(Qt.AlignCenter)
|
|
|
|
self.inference_badge = QLabel("Inference: -- ms")
|
|
self.inference_badge.setStyleSheet("background: #2980b9; color: #fff; border-radius: 12px; padding: 4px 24px; font-weight: bold; font-size: 15px;")
|
|
self.inference_badge.setAlignment(Qt.AlignCenter)
|
|
|
|
self.badge_bar.addWidget(self.fps_badge)
|
|
self.badge_bar.addSpacing(12)
|
|
self.badge_bar.addWidget(self.inference_badge)
|
|
self.badge_bar.addSpacing(18)
|
|
|
|
# Add current overlay (start with standard)
|
|
self.current_overlay = self.standard_overlay
|
|
self.badge_bar.addWidget(self.current_overlay)
|
|
self.badge_bar.addStretch()
|
|
|
|
self.overlay_layout.addLayout(self.badge_bar)
|
|
return container
|
|
|
|
def _create_control_bar(self):
|
|
"""Create control bar"""
|
|
widget = QWidget()
|
|
control_bar = QHBoxLayout(widget)
|
|
control_bar.setContentsMargins(0, 16, 0, 0)
|
|
|
|
# Playback controls
|
|
self.play_btn = QPushButton()
|
|
self.play_btn.setIcon(QIcon.fromTheme("media-playback-start"))
|
|
self.play_btn.setToolTip("Play")
|
|
self.play_btn.setFixedSize(48, 48)
|
|
self.play_btn.setEnabled(False)
|
|
self.play_btn.setStyleSheet(self._button_style())
|
|
|
|
self.pause_btn = QPushButton()
|
|
self.pause_btn.setIcon(QIcon.fromTheme("media-playback-pause"))
|
|
self.pause_btn.setToolTip("Pause")
|
|
self.pause_btn.setFixedSize(48, 48)
|
|
self.pause_btn.setEnabled(False)
|
|
self.pause_btn.setStyleSheet(self._button_style())
|
|
|
|
self.stop_btn = QPushButton()
|
|
self.stop_btn.setIcon(QIcon.fromTheme("media-playback-stop"))
|
|
self.stop_btn.setToolTip("Stop")
|
|
self.stop_btn.setFixedSize(48, 48)
|
|
self.stop_btn.setEnabled(False)
|
|
self.stop_btn.setStyleSheet(self._button_style())
|
|
|
|
for btn, sig in zip([self.play_btn, self.pause_btn, self.stop_btn],
|
|
[self.play_clicked.emit, self.pause_clicked.emit, self.stop_clicked.emit]):
|
|
btn.clicked.connect(sig)
|
|
|
|
control_bar.addWidget(self.play_btn)
|
|
control_bar.addWidget(self.pause_btn)
|
|
control_bar.addWidget(self.stop_btn)
|
|
control_bar.addSpacing(16)
|
|
|
|
# Progress bar
|
|
self.progress = QSlider(Qt.Horizontal)
|
|
self.progress.setStyleSheet("QSlider::groove:horizontal { height: 6px; background: #232323; border-radius: 3px; } QSlider::handle:horizontal { background: #03DAC5; border-radius: 8px; width: 18px; }")
|
|
self.progress.setMinimumWidth(240)
|
|
self.progress.setEnabled(False)
|
|
self.progress.valueChanged.connect(self.seek_changed.emit)
|
|
control_bar.addWidget(self.progress, 2)
|
|
|
|
self.timestamp = QLabel("00:00 / 00:00")
|
|
self.timestamp.setStyleSheet("color: #bbb; font-size: 13px;")
|
|
control_bar.addWidget(self.timestamp)
|
|
control_bar.addSpacing(16)
|
|
|
|
# Detection toggle & screenshot
|
|
self.detection_toggle = QCheckBox("Enable Detection")
|
|
self.detection_toggle.setChecked(True)
|
|
self.detection_toggle.setStyleSheet("color: #fff; font-size: 14px;")
|
|
self.detection_toggle.setEnabled(False)
|
|
self.detection_toggle.toggled.connect(self.detection_toggled.emit)
|
|
control_bar.addWidget(self.detection_toggle)
|
|
|
|
self.screenshot_btn = QPushButton()
|
|
self.screenshot_btn.setIcon(QIcon.fromTheme("camera-photo"))
|
|
self.screenshot_btn.setText("Screenshot")
|
|
self.screenshot_btn.setToolTip("Save current frame as image")
|
|
self.screenshot_btn.setEnabled(False)
|
|
self.screenshot_btn.setStyleSheet(self._button_style())
|
|
self.screenshot_btn.clicked.connect(self.screenshot_clicked.emit)
|
|
control_bar.addWidget(self.screenshot_btn)
|
|
control_bar.addStretch()
|
|
|
|
return widget
|
|
|
|
def _create_right_panel(self):
|
|
"""Create right panel for smart intersection controls"""
|
|
panel = QScrollArea()
|
|
panel.setWidgetResizable(True)
|
|
panel.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
panel.setStyleSheet("""
|
|
QScrollArea {
|
|
background: #1a1a1a;
|
|
border: 1px solid #424242;
|
|
border-radius: 8px;
|
|
}
|
|
""")
|
|
|
|
content = QWidget()
|
|
layout = QVBoxLayout(content)
|
|
layout.setContentsMargins(16, 16, 16, 16)
|
|
layout.setSpacing(16)
|
|
|
|
# Smart Intersection Controls
|
|
intersection_group = QGroupBox("🚦 Smart Intersection")
|
|
intersection_group.setStyleSheet("""
|
|
QGroupBox {
|
|
color: #03DAC5;
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
border: 2px solid #03DAC5;
|
|
border-radius: 8px;
|
|
margin-top: 12px;
|
|
padding-top: 8px;
|
|
}
|
|
QGroupBox::title {
|
|
subcontrol-origin: margin;
|
|
left: 16px;
|
|
padding: 0 8px 0 8px;
|
|
}
|
|
""")
|
|
|
|
intersection_layout = QVBoxLayout(intersection_group)
|
|
|
|
# Scene Analytics Toggle
|
|
self.scene_analytics_toggle = QCheckBox("Scene Analytics")
|
|
self.scene_analytics_toggle.setChecked(True)
|
|
self.scene_analytics_toggle.setStyleSheet("color: white; font-size: 12px;")
|
|
self.scene_analytics_toggle.toggled.connect(self.scene_analytics_toggled.emit)
|
|
intersection_layout.addWidget(self.scene_analytics_toggle)
|
|
|
|
# Multi-object tracking
|
|
self.multi_tracking_toggle = QCheckBox("Multi-Object Tracking")
|
|
self.multi_tracking_toggle.setChecked(True)
|
|
self.multi_tracking_toggle.setStyleSheet("color: white; font-size: 12px;")
|
|
intersection_layout.addWidget(self.multi_tracking_toggle)
|
|
|
|
# Speed estimation
|
|
self.speed_estimation_toggle = QCheckBox("Speed Estimation")
|
|
self.speed_estimation_toggle.setChecked(True)
|
|
self.speed_estimation_toggle.setStyleSheet("color: white; font-size: 12px;")
|
|
intersection_layout.addWidget(self.speed_estimation_toggle)
|
|
|
|
layout.addWidget(intersection_group)
|
|
|
|
# ROI Management
|
|
self.roi_widget = IntersectionROIWidget()
|
|
self.roi_widget.roi_updated.connect(self.roi_configuration_changed.emit)
|
|
layout.addWidget(self.roi_widget)
|
|
|
|
# Analytics Summary
|
|
analytics_group = QGroupBox("📊 Analytics Summary")
|
|
analytics_group.setStyleSheet(intersection_group.styleSheet().replace("#03DAC5", "#e67e22"))
|
|
analytics_layout = QVBoxLayout(analytics_group)
|
|
|
|
self.total_objects_label = QLabel("Total Objects: 0")
|
|
self.crosswalk_events_label = QLabel("Crosswalk Events: 0")
|
|
self.lane_events_label = QLabel("Lane Violations: 0")
|
|
self.safety_alerts_label = QLabel("Safety Alerts: 0")
|
|
|
|
for label in [self.total_objects_label, self.crosswalk_events_label,
|
|
self.lane_events_label, self.safety_alerts_label]:
|
|
label.setStyleSheet("color: white; font-size: 12px;")
|
|
analytics_layout.addWidget(label)
|
|
|
|
layout.addWidget(analytics_group)
|
|
|
|
# Performance Monitoring
|
|
perf_group = QGroupBox("⚡ Performance")
|
|
perf_group.setStyleSheet(intersection_group.styleSheet().replace("#03DAC5", "#9b59b6"))
|
|
perf_layout = QVBoxLayout(perf_group)
|
|
|
|
self.gpu_usage_label = QLabel("GPU Usage: -%")
|
|
self.memory_usage_label = QLabel("Memory: - MB")
|
|
self.processing_time_label = QLabel("Processing: - ms")
|
|
|
|
for label in [self.gpu_usage_label, self.memory_usage_label, self.processing_time_label]:
|
|
label.setStyleSheet("color: white; font-size: 12px;")
|
|
perf_layout.addWidget(label)
|
|
|
|
layout.addWidget(perf_group)
|
|
|
|
layout.addStretch()
|
|
|
|
panel.setWidget(content)
|
|
return panel
|
|
|
|
def _toggle_smart_intersection(self, enabled):
|
|
"""Toggle smart intersection mode"""
|
|
self.smart_intersection_mode = enabled
|
|
self.smart_intersection_enabled.emit(enabled)
|
|
|
|
# Switch overlay
|
|
if enabled:
|
|
self._switch_to_smart_overlay()
|
|
self.mode_status.setText("🚦 Smart Intersection Active")
|
|
self.mode_status.setStyleSheet("color: #03DAC5; font-weight: bold; font-size: 12px;")
|
|
else:
|
|
self._switch_to_standard_overlay()
|
|
self.mode_status.setText("Standard Detection Mode")
|
|
self.mode_status.setStyleSheet("color: #bbb; font-size: 12px;")
|
|
|
|
# Enable/disable multi-camera toggle
|
|
self.multi_camera_toggle.setEnabled(enabled)
|
|
if not enabled:
|
|
self.multi_camera_toggle.setChecked(False)
|
|
|
|
def _toggle_multi_camera(self, enabled):
|
|
"""Toggle multi-camera mode"""
|
|
self.multi_camera_mode = enabled
|
|
self.multi_camera_mode_enabled.emit(enabled)
|
|
|
|
if enabled:
|
|
self.video_tabs.setCurrentIndex(1) # Switch to multi-camera tab
|
|
self.mode_status.setText("🚦 Multi-Camera Intersection Active")
|
|
else:
|
|
self.video_tabs.setCurrentIndex(0) # Switch to single camera tab
|
|
if self.smart_intersection_mode:
|
|
self.mode_status.setText("🚦 Smart Intersection Active")
|
|
|
|
def _switch_to_smart_overlay(self):
|
|
"""Switch to smart intersection overlay"""
|
|
self.badge_bar.removeWidget(self.current_overlay)
|
|
self.current_overlay.setParent(None)
|
|
self.current_overlay = self.smart_overlay
|
|
self.badge_bar.addWidget(self.current_overlay)
|
|
|
|
def _switch_to_standard_overlay(self):
|
|
"""Switch to standard overlay"""
|
|
self.badge_bar.removeWidget(self.current_overlay)
|
|
self.current_overlay.setParent(None)
|
|
self.current_overlay = self.standard_overlay
|
|
self.badge_bar.addWidget(self.current_overlay)
|
|
|
|
def _button_style(self):
|
|
return """
|
|
QPushButton {
|
|
background: #232323;
|
|
border-radius: 24px;
|
|
color: #fff;
|
|
font-size: 15px;
|
|
border: none;
|
|
}
|
|
QPushButton:hover {
|
|
background: #03DAC5;
|
|
color: #222;
|
|
}
|
|
QPushButton:pressed {
|
|
background: #018786;
|
|
}
|
|
"""
|
|
|
|
def _select_file(self):
|
|
file_path, _ = QFileDialog.getOpenFileName(self, "Select Video File", "", "Video Files (*.mp4 *.avi *.mov *.mkv *.webm);;All Files (*)")
|
|
if file_path:
|
|
self.file_label.setText(file_path)
|
|
self.file_selected.emit(file_path)
|
|
self.video_loaded = True
|
|
self._enable_controls(True)
|
|
self.video_label.setText("")
|
|
self.auto_select_model_device.emit()
|
|
|
|
def _enable_controls(self, enabled):
|
|
self.play_btn.setEnabled(enabled)
|
|
self.pause_btn.setEnabled(enabled)
|
|
self.stop_btn.setEnabled(enabled)
|
|
self.progress.setEnabled(enabled)
|
|
self.detection_toggle.setEnabled(enabled)
|
|
self.screenshot_btn.setEnabled(enabled)
|
|
if enabled:
|
|
self.auto_select_model_device.emit()
|
|
|
|
def update_display(self, pixmap):
|
|
"""Update display with new frame"""
|
|
if pixmap:
|
|
if self.multi_camera_mode:
|
|
# In multi-camera mode, distribute to different camera views
|
|
# For now, just update the single view
|
|
scaled = pixmap.scaled(self.video_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
|
self.video_label.setPixmap(scaled)
|
|
else:
|
|
scaled = pixmap.scaled(self.video_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
|
self.video_label.setPixmap(scaled)
|
|
self._set_controls_enabled(True)
|
|
self.video_label.setStyleSheet("background: transparent; color: #888; font-size: 18px;")
|
|
else:
|
|
self.video_label.clear()
|
|
self.video_label.setText("No video loaded. Please select a video file.")
|
|
self._set_controls_enabled(False)
|
|
self.video_label.setStyleSheet("background: transparent; color: #F44336; font-size: 18px;")
|
|
|
|
def _set_controls_enabled(self, enabled):
|
|
for btn in [self.play_btn, self.pause_btn, self.stop_btn, self.progress, self.detection_toggle, self.screenshot_btn]:
|
|
btn.setEnabled(enabled)
|
|
|
|
def update_stats(self, stats):
|
|
"""Update statistics display"""
|
|
if self.smart_intersection_mode:
|
|
# Update smart intersection overlay
|
|
scene_data = {
|
|
'active_tracks': stats.get('total_objects', 0),
|
|
'roi_events': stats.get('roi_events', 0),
|
|
'cameras': {
|
|
'north': stats.get('north_objects', 0),
|
|
'east': stats.get('east_objects', 0),
|
|
'south': stats.get('south_objects', 0),
|
|
'west': stats.get('west_objects', 0)
|
|
},
|
|
'analytics': {
|
|
'crosswalk_events': stats.get('crosswalk_events', 0),
|
|
'lane_events': stats.get('lane_events', 0),
|
|
'safety_events': stats.get('safety_events', 0)
|
|
}
|
|
}
|
|
self.smart_overlay.update_smart_intersection(scene_data)
|
|
|
|
# Update right panel analytics
|
|
self.total_objects_label.setText(f"Total Objects: {stats.get('total_objects', 0)}")
|
|
self.crosswalk_events_label.setText(f"Crosswalk Events: {stats.get('crosswalk_events', 0)}")
|
|
self.lane_events_label.setText(f"Lane Violations: {stats.get('lane_events', 0)}")
|
|
self.safety_alerts_label.setText(f"Safety Alerts: {stats.get('safety_events', 0)}")
|
|
else:
|
|
# Update standard overlay
|
|
cars = stats.get('cars', 0)
|
|
trucks = stats.get('trucks', 0)
|
|
peds = stats.get('peds', 0)
|
|
tlights = stats.get('tlights', 0)
|
|
motorcycles = stats.get('motorcycles', 0)
|
|
model = stats.get('model', stats.get('model_name', '-'))
|
|
device = stats.get('device', stats.get('device_name', '-'))
|
|
self.standard_overlay.update_overlay(model, device, cars, trucks, peds, tlights, motorcycles)
|
|
|
|
# Update performance badges
|
|
fps = stats.get('fps', None)
|
|
inference = stats.get('inference', stats.get('detection_time', stats.get('detection_time_ms', None)))
|
|
|
|
if fps is not None:
|
|
self.fps_badge.setText(f"FPS: {fps:.2f}")
|
|
else:
|
|
self.fps_badge.setText("FPS: --")
|
|
|
|
if inference is not None:
|
|
self.inference_badge.setText(f"Inference: {inference:.1f} ms")
|
|
else:
|
|
self.inference_badge.setText("Inference: -- ms")
|
|
|
|
# Update performance panel
|
|
self.gpu_usage_label.setText(f"GPU Usage: {stats.get('gpu_usage', 0):.1f}%")
|
|
self.memory_usage_label.setText(f"Memory: {stats.get('memory_usage', 0):.1f} MB")
|
|
self.processing_time_label.setText(f"Processing: {stats.get('processing_time', 0):.1f} ms")
|
|
|
|
def update_progress(self, value, max_value, timestamp):
|
|
self.progress.setMaximum(max_value)
|
|
self.progress.setValue(value)
|
|
if isinstance(timestamp, float) or isinstance(timestamp, int):
|
|
timestamp_str = f"{timestamp:.2f}"
|
|
else:
|
|
timestamp_str = str(timestamp)
|
|
self.timestamp.setText(timestamp_str)
|
|
|
|
def update_multi_camera_feed(self, camera_position, pixmap, object_count=0):
|
|
"""Update specific camera feed in multi-camera mode"""
|
|
if self.multi_camera_mode:
|
|
self.multi_cam_widget.update_camera_feed(camera_position, pixmap, object_count)
|
|
|
|
def get_smart_intersection_config(self):
|
|
"""Get current smart intersection configuration"""
|
|
return {
|
|
'enabled': self.smart_intersection_mode,
|
|
'multi_camera': self.multi_camera_mode,
|
|
'scene_analytics': self.scene_analytics_toggle.isChecked(),
|
|
'multi_tracking': self.multi_tracking_toggle.isChecked(),
|
|
'speed_estimation': self.speed_estimation_toggle.isChecked()
|
|
}
|