from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QTableWidget, QTableWidgetItem, QTabWidget, QScrollArea, QFrame, QGridLayout, QPushButton, QTextEdit ) from PySide6.QtCore import Signal, Qt, QTimer from PySide6.QtGui import QFont, QColor, QPalette import re import json from datetime import datetime from collections import defaultdict, deque class AnalyticsTable(QTableWidget): """Enhanced table widget for analytics data""" def __init__(self, headers): super().__init__() self.setColumnCount(len(headers)) self.setHorizontalHeaderLabels(headers) self.setAlternatingRowColors(True) self.setStyleSheet(""" QTableWidget { background-color: #1e1e1e; color: #ffffff; gridline-color: #404040; border: 1px solid #404040; border-radius: 8px; } QTableWidget::item { padding: 8px; border-bottom: 1px solid #404040; } QTableWidget::item:selected { background-color: #03DAC5; color: #000000; } QHeaderView::section { background-color: #2d2d2d; color: #ffffff; padding: 10px; border: none; font-weight: bold; } """) self.verticalHeader().setVisible(False) self.horizontalHeader().setStretchLastSection(True) def add_row_data(self, data): row = self.rowCount() self.insertRow(row) for col, value in enumerate(data): item = QTableWidgetItem(str(value)) item.setTextAlignment(Qt.AlignCenter) self.setItem(row, col, item) class StatsCard(QFrame): """Statistics card widget""" def __init__(self, title, value, color="#03DAC5"): super().__init__() self.setFixedHeight(120) self.setStyleSheet(f""" QFrame {{ background-color: #2d2d2d; border: 2px solid {color}; border-radius: 12px; padding: 16px; }} """) layout = QVBoxLayout(self) self.title_label = QLabel(title) self.title_label.setStyleSheet(f"color: {color}; font-size: 14px; font-weight: bold;") self.title_label.setAlignment(Qt.AlignCenter) self.value_label = QLabel(str(value)) self.value_label.setStyleSheet("color: #ffffff; font-size: 24px; font-weight: bold;") self.value_label.setAlignment(Qt.AlignCenter) layout.addWidget(self.title_label) layout.addWidget(self.value_label) def update_value(self, value): self.value_label.setText(str(value)) class LogAnalyzer: """Analyzes debug logs and extracts structured data""" @staticmethod def parse_match_debug(log_text): """Parse vehicle matching debug logs""" matches = [] pattern = r'\[MATCH SUCCESS\] Detection at \(([^)]+)\) matched with track ID=(\d+)\s*-> STATUS: moving=(\w+), violating=(\w+), IoU=([^,]+), distance=([^)]+)' for match in re.finditer(pattern, log_text): position, track_id, moving, violating, iou, distance = match.groups() matches.append({ 'position': f"({position})", 'track_id': track_id, 'iou': float(iou), 'distance': float(distance), 'status': '✅ Matched', 'moving': moving == 'True', 'violating': violating == 'True' }) # Parse failed matches failed_pattern = r'\[MATCH FAILED\] No suitable match found for car detection at \(([^)]+)\)' for match in re.finditer(failed_pattern, log_text): position = match.group(1) matches.append({ 'position': f"({position})", 'track_id': '—', 'iou': '—', 'distance': '—', 'status': '❌ Unmatched', 'moving': False, 'violating': False }) return matches @staticmethod def parse_traffic_lights(log_text): """Parse traffic light detection logs""" lights = [] pattern = r'\[DEBUG\] ratios: red=([^,]+), yellow=([^,]+), green=([^,]+).*?📝 Drawing traffic light status: (\w+) at bbox \[([^\]]+)\]' for i, match in enumerate(re.finditer(pattern, log_text, re.DOTALL)): red_ratio, yellow_ratio, green_ratio, status, bbox = match.groups() lights.append({ 'detection_id': f"Traffic Light {i+1}", 'red_ratio': float(red_ratio), 'yellow_ratio': float(yellow_ratio), 'green_ratio': float(green_ratio), 'status': f"🔴 {status.title()}" if status == 'red' else f"🟡 {status.title()}" if status == 'yellow' else f"🟢 {status.title()}", 'bbox': bbox }) return lights @staticmethod def parse_violations(log_text): """Parse violation logs""" violations = [] pattern = r'🚨 Emitting RED LIGHT VIOLATION: Track ID (\d+)' for match in re.finditer(pattern, log_text): track_id = match.group(1) violations.append({ 'track_id': track_id, 'violation_type': '🚨 Red Light Violation', 'timestamp': datetime.now().strftime("%H:%M:%S") }) # Parse crosswalk violations crosswalk_pattern = r'\[VIOLATION\] Improper stop on crosswalk: Vehicle ID=(\d+) stopped on crosswalk during red light \(overlap=([^,]+), speed=([^)]+)\)' for match in re.finditer(crosswalk_pattern, log_text): track_id, overlap, speed = match.groups() violations.append({ 'track_id': track_id, 'violation_type': '🚧 Crosswalk Violation', 'timestamp': datetime.now().strftime("%H:%M:%S"), 'details': f"Overlap: {overlap}, Speed: {speed}" }) return violations @staticmethod def parse_performance_stats(log_text): """Parse performance statistics""" stats = {} # Extract FPS fps_match = re.search(r'FPS: ([\d.]+)', log_text) if fps_match: stats['fps'] = float(fps_match.group(1)) # Extract inference time inference_match = re.search(r'Inference: ([\d.]+)ms', log_text) if inference_match: stats['inference_time'] = float(inference_match.group(1)) # Extract device device_match = re.search(r"'device': '(\w+)'", log_text) if device_match: stats['device'] = device_match.group(1) # Extract vehicle counts vehicle_match = re.search(r'Vehicles: (\d+) with IDs, (\d+) without IDs', log_text) if vehicle_match: stats['tracked_vehicles'] = int(vehicle_match.group(1)) stats['untracked_vehicles'] = int(vehicle_match.group(2)) # Extract detection counts detection_match = re.search(r'Detections count: (\d+)', log_text) if detection_match: stats['total_detections'] = int(detection_match.group(1)) return stats @staticmethod def parse_bytetrack_stats(log_text): """Parse ByteTrack performance statistics""" stats = {} # Extract tracking states state_match = re.search(r'Current state: (\d+) tracked, (\d+) lost', log_text) if state_match: stats['tracked_count'] = int(state_match.group(1)) stats['lost_count'] = int(state_match.group(2)) # Extract matching results matched_match = re.search(r'Matched (\d+) tracks, created (\d+) new tracks, removed (\d+) expired tracks', log_text) if matched_match: stats['matched_tracks'] = int(matched_match.group(1)) stats['new_tracks'] = int(matched_match.group(2)) stats['removed_tracks'] = int(matched_match.group(3)) return stats class AnalyticsDashboard(QWidget): """Comprehensive analytics dashboard for video detection data""" def __init__(self): super().__init__() self.log_buffer = deque(maxlen=1000) # Store recent logs self.setup_ui() self.setup_update_timer() def setup_ui(self): """Setup the dashboard UI""" main_layout = QVBoxLayout(self) # Header header = QLabel("🔍 Video Detection Analytics Dashboard") header.setStyleSheet("font-size: 24px; font-weight: bold; color: #03DAC5; padding: 16px;") header.setAlignment(Qt.AlignCenter) main_layout.addWidget(header) # Stats cards stats_layout = QHBoxLayout() self.fps_card = StatsCard("FPS", "0.0", "#27ae60") self.inference_card = StatsCard("Inference (ms)", "0.0", "#3498db") self.vehicles_card = StatsCard("Tracked Vehicles", "0", "#e74c3c") self.violations_card = StatsCard("Violations", "0", "#f39c12") for card in [self.fps_card, self.inference_card, self.vehicles_card, self.violations_card]: stats_layout.addWidget(card) main_layout.addLayout(stats_layout) # Tabs for different analytics self.tab_widget = QTabWidget() self.tab_widget.setStyleSheet(""" QTabWidget::pane { border: 1px solid #404040; background-color: #1e1e1e; } QTabBar::tab { background-color: #2d2d2d; color: #ffffff; padding: 10px 20px; margin-right: 2px; } QTabBar::tab:selected { background-color: #03DAC5; color: #000000; } """) # Vehicle Matching Tab self.matching_tab = self.create_matching_tab() self.tab_widget.addTab(self.matching_tab, "🚗 Vehicle Matching") # Traffic Light Tab self.traffic_light_tab = self.create_traffic_light_tab() self.tab_widget.addTab(self.traffic_light_tab, "🚦 Traffic Lights") # Violations Tab self.violations_tab = self.create_violations_tab() self.tab_widget.addTab(self.violations_tab, "⚠️ Violations") # Performance Tab self.performance_tab = self.create_performance_tab() self.tab_widget.addTab(self.performance_tab, "📊 Performance") # Raw Logs Tab self.logs_tab = self.create_logs_tab() self.tab_widget.addTab(self.logs_tab, "📝 Raw Logs") main_layout.addWidget(self.tab_widget) def create_matching_tab(self): """Create vehicle matching analytics tab""" widget = QWidget() layout = QVBoxLayout(widget) # Matching table self.matching_table = AnalyticsTable([ "Detection Position", "Track ID", "IoU", "Distance", "Match Status", "Moving", "Violating" ]) layout.addWidget(self.matching_table) return widget def create_traffic_light_tab(self): """Create traffic light analytics tab""" widget = QWidget() layout = QVBoxLayout(widget) # Traffic light table self.traffic_light_table = AnalyticsTable([ "Detection ID", "Red Ratio", "Yellow Ratio", "Green Ratio", "Status", "Bounding Box" ]) layout.addWidget(self.traffic_light_table) return widget def create_violations_tab(self): """Create violations analytics tab""" widget = QWidget() layout = QVBoxLayout(widget) # Violations table self.violations_table = AnalyticsTable([ "Track ID", "Violation Type", "Timestamp", "Details" ]) layout.addWidget(self.violations_table) return widget def create_performance_tab(self): """Create performance analytics tab""" widget = QWidget() layout = QVBoxLayout(widget) # Performance table self.performance_table = AnalyticsTable([ "Metric", "Current Value", "Unit", "Status" ]) layout.addWidget(self.performance_table) # ByteTrack stats self.bytetrack_table = AnalyticsTable([ "Metric", "Value", "Description" ]) layout.addWidget(self.bytetrack_table) return widget def create_logs_tab(self): """Create raw logs display tab""" widget = QWidget() layout = QVBoxLayout(widget) # Control buttons controls = QHBoxLayout() clear_btn = QPushButton("Clear Logs") clear_btn.clicked.connect(self.clear_logs) clear_btn.setStyleSheet("padding: 8px 16px; background: #e74c3c; color: white; border-radius: 4px;") export_btn = QPushButton("Export Logs") export_btn.clicked.connect(self.export_logs) export_btn.setStyleSheet("padding: 8px 16px; background: #27ae60; color: white; border-radius: 4px;") controls.addWidget(clear_btn) controls.addWidget(export_btn) controls.addStretch() layout.addLayout(controls) # Logs display self.logs_display = QTextEdit() self.logs_display.setReadOnly(True) self.logs_display.setStyleSheet(""" QTextEdit { background-color: #0d1117; color: #c9d1d9; font-family: 'Consolas', 'SF Mono', monospace; font-size: 12px; border: 1px solid #404040; border-radius: 8px; padding: 8px; } """) layout.addWidget(self.logs_display) return widget def setup_update_timer(self): """Setup timer for periodic updates""" self.update_timer = QTimer() self.update_timer.timeout.connect(self.process_logs) self.update_timer.start(1000) # Update every second def add_log_data(self, log_text): """Add new log data to the buffer""" self.log_buffer.append({ 'timestamp': datetime.now(), 'content': log_text }) # Update raw logs display self.logs_display.append(f"[{datetime.now().strftime('%H:%M:%S')}] {log_text}") def process_logs(self): """Process buffered logs and update analytics""" if not self.log_buffer: return # Combine recent logs recent_logs = '\n'.join([log['content'] for log in list(self.log_buffer)[-50:]]) # Update matching analytics self.update_matching_analytics(recent_logs) # Update traffic light analytics self.update_traffic_light_analytics(recent_logs) # Update violations analytics self.update_violations_analytics(recent_logs) # Update performance analytics self.update_performance_analytics(recent_logs) def update_matching_analytics(self, log_text): """Update vehicle matching analytics""" matches = LogAnalyzer.parse_match_debug(log_text) # Clear and repopulate table self.matching_table.setRowCount(0) for match in matches[-20:]: # Show last 20 matches row_data = [ match['position'], match['track_id'], match['iou'], match['distance'], match['status'], '✅' if match['moving'] else '❌', '⚠️' if match['violating'] else '✅' ] self.matching_table.add_row_data(row_data) def update_traffic_light_analytics(self, log_text): """Update traffic light analytics""" lights = LogAnalyzer.parse_traffic_lights(log_text) # Clear and repopulate table self.traffic_light_table.setRowCount(0) for light in lights[-10:]: # Show last 10 detections row_data = [ light['detection_id'], f"{light['red_ratio']:.3f}", f"{light['yellow_ratio']:.3f}", f"{light['green_ratio']:.3f}", light['status'], light['bbox'] ] self.traffic_light_table.add_row_data(row_data) def update_violations_analytics(self, log_text): """Update violations analytics""" violations = LogAnalyzer.parse_violations(log_text) # Update violations count card total_violations = len(violations) self.violations_card.update_value(total_violations) # Clear and repopulate table self.violations_table.setRowCount(0) for violation in violations[-20:]: # Show last 20 violations row_data = [ violation['track_id'], violation['violation_type'], violation['timestamp'], violation.get('details', '—') ] self.violations_table.add_row_data(row_data) def update_performance_analytics(self, log_text): """Update performance analytics""" stats = LogAnalyzer.parse_performance_stats(log_text) bytetrack_stats = LogAnalyzer.parse_bytetrack_stats(log_text) # Update stats cards if 'fps' in stats: self.fps_card.update_value(f"{stats['fps']:.2f}") if 'inference_time' in stats: self.inference_card.update_value(f"{stats['inference_time']:.1f}") if 'tracked_vehicles' in stats: self.vehicles_card.update_value(stats['tracked_vehicles']) # Update performance table self.performance_table.setRowCount(0) performance_metrics = [ ("FPS", f"{stats.get('fps', 0):.2f}", "frames/sec", "🟢 Good" if stats.get('fps', 0) > 5 else "🔴 Low"), ("Inference Time", f"{stats.get('inference_time', 0):.1f}", "ms", "🟢 Fast" if stats.get('inference_time', 0) < 100 else "🟡 Slow"), ("Device", stats.get('device', 'Unknown'), "—", "🟢 GPU" if stats.get('device') == 'GPU' else "🟡 CPU"), ("Total Detections", stats.get('total_detections', 0), "objects", "🟢 Active"), ("Tracked Vehicles", stats.get('tracked_vehicles', 0), "vehicles", "🟢 Active"), ("Untracked Vehicles", stats.get('untracked_vehicles', 0), "vehicles", "🟡 Unmatched" if stats.get('untracked_vehicles', 0) > 0 else "🟢 All Tracked") ] for metric in performance_metrics: self.performance_table.add_row_data(metric) # Update ByteTrack table self.bytetrack_table.setRowCount(0) bytetrack_metrics = [ ("Tracked Objects", bytetrack_stats.get('tracked_count', 0), "Currently being tracked"), ("Lost Objects", bytetrack_stats.get('lost_count', 0), "Lost tracking but may recover"), ("Matched Tracks", bytetrack_stats.get('matched_tracks', 0), "Successfully matched this frame"), ("New Tracks", bytetrack_stats.get('new_tracks', 0), "New objects started tracking"), ("Removed Tracks", bytetrack_stats.get('removed_tracks', 0), "Expired tracks removed") ] for metric in bytetrack_metrics: self.bytetrack_table.add_row_data(metric) def clear_logs(self): """Clear all logs""" self.log_buffer.clear() self.logs_display.clear() def export_logs(self): """Export logs to file""" from PySide6.QtWidgets import QFileDialog file_path, _ = QFileDialog.getSaveFileName( self, "Export Logs", "analytics_logs.txt", "Text Files (*.txt)" ) if file_path: with open(file_path, 'w') as f: for log in self.log_buffer: f.write(f"[{log['timestamp']}] {log['content']}\n")