Clean push: Removed heavy files & added only latest snapshot

This commit is contained in:
2025-07-26 05:16:12 +05:30
commit acf84e8767
250 changed files with 58564 additions and 0 deletions

1576
qt_app_pyside1/ui/UI.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
# UI package for Traffic Monitoring System

Binary file not shown.

View File

@@ -0,0 +1,662 @@
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QGroupBox, QPushButton, QScrollArea, QSplitter
)
from PySide6.QtCore import Qt, Slot
from PySide6.QtCharts import QChart, QChartView, QLineSeries, QPieSeries, QBarSeries, QBarSet, QBarCategoryAxis, QScatterSeries, QValueAxis
from PySide6.QtGui import QPainter, QColor, QPen, QFont, QBrush, QLinearGradient, QGradient
class ChartWidget(QWidget):
"""Base widget for analytics charts"""
def __init__(self, title):
super().__init__()
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
# Chart title
self.title_label = QLabel(title)
self.title_label.setAlignment(Qt.AlignCenter)
self.title_label.setStyleSheet("font-weight: bold; font-size: 14px;")
self.layout.addWidget(self.title_label)
# Create chart
self.chart = QChart()
self.chart.setAnimationOptions(QChart.SeriesAnimations)
self.chart.setBackgroundBrush(QBrush(QColor(240, 240, 240)))
self.chart.legend().setVisible(True)
self.chart.legend().setAlignment(Qt.AlignBottom)
# Chart view
self.chartview = QChartView(self.chart)
self.chartview.setRenderHint(QPainter.RenderHint.Antialiasing)
self.layout.addWidget(self.chartview)
self.setMinimumSize(400, 300)
class TimeSeriesChart(ChartWidget):
"""Time series chart for traffic data"""
def __init__(self, title="Traffic Over Time"):
super().__init__(title)
# Create series
self.vehicle_series = QLineSeries()
self.vehicle_series.setName("Vehicles")
self.vehicle_series.setPen(QPen(QColor(0, 162, 232), 2))
self.pedestrian_series = QLineSeries()
self.pedestrian_series.setName("Pedestrians")
self.pedestrian_series.setPen(QPen(QColor(255, 140, 0), 2))
self.violation_series = QLineSeries()
self.violation_series.setName("Violations")
self.violation_series.setPen(QPen(QColor(232, 0, 0), 2))
self.traffic_light_color_series = QLineSeries()
self.traffic_light_color_series.setName("Traffic Light Color")
self.traffic_light_color_series.setPen(QPen(QColor(128, 0, 128), 2, Qt.DashLine))
# Add series to chart
self.chart.addSeries(self.vehicle_series)
self.chart.addSeries(self.pedestrian_series)
self.chart.addSeries(self.violation_series)
self.chart.addSeries(self.traffic_light_color_series)
# Create and configure axes
self.chart.createDefaultAxes()
x_axis = self.chart.axes(Qt.Horizontal)[0]
x_axis.setTitleText("Time")
x_axis.setGridLineVisible(True)
x_axis.setLabelsAngle(45)
y_axis = self.chart.axes(Qt.Vertical)[0]
y_axis.setTitleText("Count")
y_axis.setGridLineVisible(True)
def update_data(self, time_series):
"""Update chart with new time series data"""
try:
if not time_series or 'timestamps' not in time_series:
return
# Check if chart and series are still valid
if not hasattr(self, 'chart') or self.chart is None:
return
if not hasattr(self, 'vehicle_series') or self.vehicle_series is None:
return
timestamps = time_series.get('timestamps', [])
vehicle_counts = time_series.get('vehicle_counts', [])
pedestrian_counts = time_series.get('pedestrian_counts', [])
violation_counts = time_series.get('violation_counts', [])
traffic_light_colors = time_series.get('traffic_light_colors', [])
# Clear existing series safely
try:
self.vehicle_series.clear()
self.pedestrian_series.clear()
self.violation_series.clear()
self.traffic_light_color_series.clear()
except RuntimeError:
# C++ object was already deleted, skip update
return
# Add data points
for i in range(len(timestamps)):
try:
# Add x as index, y as count
self.vehicle_series.append(i, vehicle_counts[i] if i < len(vehicle_counts) else 0)
self.pedestrian_series.append(i, pedestrian_counts[i] if i < len(pedestrian_counts) else 0)
self.violation_series.append(i, violation_counts[i] if i < len(violation_counts) else 0)
# Add traffic light color as mapped int for charting (0=unknown, 1=red, 2=yellow, 3=green)
if i < len(traffic_light_colors):
color_map = {'unknown': 0, 'red': 1, 'yellow': 2, 'green': 3}
color_val = color_map.get(traffic_light_colors[i], 0)
self.traffic_light_color_series.append(i, color_val)
except RuntimeError:
# C++ object was deleted during update
return
# Update axes safely
try:
axes = self.chart.axes(Qt.Horizontal)
if axes:
axes[0].setRange(0, max(len(timestamps)-1, 10))
max_count = max(
max(vehicle_counts) if vehicle_counts else 0,
max(pedestrian_counts) if pedestrian_counts else 0,
max(violation_counts) if violation_counts else 0
)
axes = self.chart.axes(Qt.Vertical)
if axes:
axes[0].setRange(0, max(max_count+1, 5))
except (RuntimeError, IndexError):
# Chart axes were deleted or not available
pass
# Optionally, set y-axis label for traffic light color
axes = self.chart.axes(Qt.Vertical)
if axes:
axes[0].setTitleText("Count / TL Color (0=U,1=R,2=Y,3=G)")
except Exception as e:
print(f"[WARNING] Chart update failed: {e}")
class DetectionPieChart(ChartWidget):
"""Pie chart for detected object classes"""
def __init__(self, title="Detection Classes"):
super().__init__(title)
self.pie_series = QPieSeries()
self.chart.addSeries(self.pie_series)
def update_data(self, detection_counts):
"""Update chart with detection counts"""
try:
if not detection_counts:
return
# Check if chart and series are still valid
if not hasattr(self, 'chart') or self.chart is None:
return
if not hasattr(self, 'pie_series') or self.pie_series is None:
return
# Clear existing slices safely
try:
self.pie_series.clear()
except RuntimeError:
# C++ object was already deleted, skip update
return
# Add new slices
for class_name, count in detection_counts.items():
# Only add if count > 0
if count > 0:
try:
slice = self.pie_series.append(class_name, count)
# Set colors based on class
if class_name.lower() == 'car':
slice.setBrush(QColor(0, 200, 0))
elif class_name.lower() == 'person':
slice.setBrush(QColor(255, 165, 0))
elif class_name.lower() == 'truck':
slice.setBrush(QColor(0, 100, 200))
elif class_name.lower() == 'bus':
slice.setBrush(QColor(200, 0, 100))
# Highlight important slices
if count > 10:
slice.setExploded(True)
slice.setLabelVisible(True)
except RuntimeError:
# C++ object was deleted during update
return
except Exception as e:
print(f"[WARNING] Pie chart update failed: {e}")
class ViolationBarChart(ChartWidget):
"""Bar chart for violation types"""
def __init__(self, title="Violations by Type"):
super().__init__(title)
# Create series
self.bar_series = QBarSeries()
self.chart.addSeries(self.bar_series)
# Create axes
self.axis_x = QBarCategoryAxis()
self.chart.addAxis(self.axis_x, Qt.AlignBottom)
self.bar_series.attachAxis(self.axis_x)
self.chart.createDefaultAxes()
self.chart.axes(Qt.Vertical)[0].setTitleText("Count")
def update_data(self, violation_counts):
"""Update chart with violation counts"""
try:
if not violation_counts:
return
# Check if chart and series are still valid
if not hasattr(self, 'chart') or self.chart is None:
return
if not hasattr(self, 'bar_series') or self.bar_series is None:
return
if not hasattr(self, 'axis_x') or self.axis_x is None:
return
# Clear existing data safely
try:
self.bar_series.clear()
except RuntimeError:
# C++ object was already deleted, skip update
return
# Create bar set
bar_set = QBarSet("Violations")
# Set colors
try:
bar_set.setColor(QColor(232, 0, 0))
except RuntimeError:
return
# Add values
values = []
categories = []
for violation_type, count in violation_counts.items():
if count > 0:
values.append(count)
# Format violation type for display
display_name = violation_type.replace('_', ' ').title()
categories.append(display_name)
if values:
try:
bar_set.append(values)
self.bar_series.append(bar_set)
# Update x-axis categories
self.axis_x.setCategories(categories)
# Update y-axis range
y_axes = self.chart.axes(Qt.Vertical)
if y_axes:
y_axes[0].setRange(0, max(values) * 1.2)
except RuntimeError:
# C++ object was deleted during update
return
except Exception as e:
print(f"[WARNING] Bar chart update failed: {e}")
class LatencyChartWidget(ChartWidget):
"""Custom latency chart with spikes, device/res changes, and live stats legend."""
def __init__(self, title="Inference Latency Over Time"):
super().__init__(title)
self.chart.setBackgroundBrush(QBrush(QColor(24, 28, 32)))
self.title_label.setStyleSheet("font-weight: bold; font-size: 16px; color: #fff;")
self.chart.legend().setVisible(False)
# Main latency line
self.latency_series = QLineSeries()
self.latency_series.setName("Latency (ms)")
self.latency_series.setPen(QPen(QColor(0, 255, 255), 2))
self.chart.addSeries(self.latency_series)
# Spikes as red dots
self.spike_series = QScatterSeries()
self.spike_series.setName("Spikes")
self.spike_series.setMarkerSize(8)
self.spike_series.setColor(QColor(255, 64, 64))
self.chart.addSeries(self.spike_series)
# Device/resolution change lines (vertical)
self.event_lines = []
# Axes
self.chart.createDefaultAxes()
self.x_axis = self.chart.axes(Qt.Horizontal)[0]
self.x_axis.setTitleText("")
self.x_axis.setLabelsColor(QColor("#fff"))
self.x_axis.setGridLineColor(QColor("#444"))
self.y_axis = self.chart.axes(Qt.Vertical)[0]
self.y_axis.setTitleText("ms")
self.y_axis.setLabelsColor(QColor("#fff"))
self.y_axis.setGridLineColor(QColor("#444"))
# Stats label
self.stats_label = QLabel()
self.stats_label.setStyleSheet("color: #00e6ff; font-size: 13px; font-weight: bold; margin: 2px 0 0 8px;")
self.layout.addWidget(self.stats_label)
def update_data(self, latency_data):
"""
latency_data: dict with keys:
'latencies': list of float,
'spike_indices': list of int,
'device_switches': list of int,
'resolution_changes': list of int
"""
if not latency_data or 'latencies' not in latency_data:
return
latencies = latency_data.get('latencies', [])
spikes = set(latency_data.get('spike_indices', []))
device_switches = set(latency_data.get('device_switches', []))
res_changes = set(latency_data.get('resolution_changes', []))
# Clear series
self.latency_series.clear()
self.spike_series.clear()
# Remove old event lines
for line in self.event_lines:
self.chart.removeAxis(line)
self.event_lines = []
# Plot latency and spikes
for i, val in enumerate(latencies):
self.latency_series.append(i, val)
if i in spikes:
self.spike_series.append(i, val)
# Add device/resolution change lines
for idx in device_switches:
line = QLineSeries()
line.setPen(QPen(QColor(33, 150, 243), 3)) # Blue
line.append(idx, min(latencies) if latencies else 0)
line.append(idx, max(latencies) if latencies else 1)
self.chart.addSeries(line)
line.attachAxis(self.x_axis)
line.attachAxis(self.y_axis)
self.event_lines.append(line)
for idx in res_changes:
line = QLineSeries()
line.setPen(QPen(QColor(255, 167, 38), 3)) # Orange
line.append(idx, min(latencies) if latencies else 0)
line.append(idx, max(latencies) if latencies else 1)
self.chart.addSeries(line)
line.attachAxis(self.x_axis)
line.attachAxis(self.y_axis)
self.event_lines.append(line)
# Update axes
self.x_axis.setRange(0, max(len(latencies)-1, 10))
self.y_axis.setRange(0, max(max(latencies) if latencies else 1, 10))
# Stats
if latencies:
avg = sum(latencies)/len(latencies)
mx = max(latencies)
self.stats_label.setText(f"Avg: {avg:.1f}ms | Max: {mx:.1f}ms | Spikes: {len(spikes)}")
else:
self.stats_label.setText("")
class FPSChartWidget(ChartWidget):
"""FPS & Resolution Impact chart with device/resolution change lines and live stats."""
def __init__(self, title="FPS & Resolution Impact"):
super().__init__(title)
self.chart.setBackgroundBrush(QBrush(QColor(24, 28, 32)))
self.title_label.setStyleSheet("font-weight: bold; font-size: 16px; color: #fff;")
self.chart.legend().setVisible(False)
self.fps_series = QLineSeries()
self.fps_series.setName("FPS")
self.fps_series.setPen(QPen(QColor(0, 255, 255), 2))
self.chart.addSeries(self.fps_series)
self.event_lines = []
self.chart.createDefaultAxes()
self.x_axis = self.chart.axes(Qt.Horizontal)[0]
self.x_axis.setLabelsColor(QColor("#fff"))
self.x_axis.setGridLineColor(QColor("#444"))
self.y_axis = self.chart.axes(Qt.Vertical)[0]
self.y_axis.setTitleText("FPS")
self.y_axis.setLabelsColor(QColor("#fff"))
self.y_axis.setGridLineColor(QColor("#444"))
self.stats_label = QLabel()
self.stats_label.setStyleSheet("color: #00ff82; font-size: 13px; font-weight: bold; margin: 2px 0 0 8px;")
self.layout.addWidget(self.stats_label)
def update_data(self, fps_data):
if not fps_data or 'fps' not in fps_data:
return
fps = fps_data.get('fps', [])
device_switches = set(fps_data.get('device_switches', []))
res_changes = set(fps_data.get('resolution_changes', []))
device_labels = fps_data.get('device_labels', {})
res_labels = fps_data.get('resolution_labels', {})
self.fps_series.clear()
for line in self.event_lines:
self.chart.removeAxis(line)
self.event_lines = []
for i, val in enumerate(fps):
self.fps_series.append(i, val)
for idx in device_switches:
line = QLineSeries()
line.setPen(QPen(QColor(33, 150, 243), 3))
line.append(idx, min(fps) if fps else 0)
line.append(idx, max(fps) if fps else 1)
self.chart.addSeries(line)
line.attachAxis(self.x_axis)
line.attachAxis(self.y_axis)
self.event_lines.append(line)
for idx in res_changes:
line = QLineSeries()
line.setPen(QPen(QColor(255, 167, 38), 3))
line.append(idx, min(fps) if fps else 0)
line.append(idx, max(fps) if fps else 1)
self.chart.addSeries(line)
line.attachAxis(self.x_axis)
line.attachAxis(self.y_axis)
self.event_lines.append(line)
self.x_axis.setRange(0, max(len(fps)-1, 10))
self.y_axis.setRange(0, max(max(fps) if fps else 1, 10))
# Live stats (current FPS, resolution, device)
cur_fps = fps[-1] if fps else 0
cur_res = res_labels.get(len(fps)-1, "-")
cur_dev = device_labels.get(len(fps)-1, "-")
self.stats_label.setText(f"Current FPS: {cur_fps:.1f} | Resolution: {cur_res} | Device: {cur_dev}")
class DeviceSwitchChartWidget(ChartWidget):
"""Device Switching & Resolution Changes chart with colored vertical lines and legend."""
def __init__(self, title="Device Switching & Resolution Changes"):
super().__init__(title)
self.chart.setBackgroundBrush(QBrush(QColor(24, 28, 32)))
self.title_label.setStyleSheet("font-weight: bold; font-size: 16px; color: #fff;")
self.chart.legend().setVisible(False)
self.event_lines = []
self.chart.createDefaultAxes()
self.x_axis = self.chart.axes(Qt.Horizontal)[0]
self.x_axis.setLabelsColor(QColor("#fff"))
self.x_axis.setGridLineColor(QColor("#444"))
self.y_axis = self.chart.axes(Qt.Vertical)[0]
self.y_axis.setTitleText("-")
self.y_axis.setLabelsColor(QColor("#fff"))
self.y_axis.setGridLineColor(QColor("#444"))
self.legend_label = QLabel()
self.legend_label.setStyleSheet("color: #ffb300; font-size: 13px; font-weight: bold; margin: 2px 0 0 8px;")
self.layout.addWidget(self.legend_label)
def update_data(self, event_data):
if not event_data:
return
cpu_spikes = set(event_data.get('cpu_spikes', []))
gpu_spikes = set(event_data.get('gpu_spikes', []))
switches = set(event_data.get('switches', []))
res_changes = set(event_data.get('res_changes', []))
n = event_data.get('n', 100)
for line in self.event_lines:
self.chart.removeAxis(line)
self.event_lines = []
for idx in cpu_spikes:
line = QLineSeries()
line.setPen(QPen(QColor(255, 64, 64), 2))
line.append(idx, 0)
line.append(idx, 1)
self.chart.addSeries(line)
line.attachAxis(self.x_axis)
line.attachAxis(self.y_axis)
self.event_lines.append(line)
for idx in gpu_spikes:
line = QLineSeries()
line.setPen(QPen(QColor(255, 87, 34), 2))
line.append(idx, 0)
line.append(idx, 1)
self.chart.addSeries(line)
line.attachAxis(self.x_axis)
line.attachAxis(self.y_axis)
self.event_lines.append(line)
for idx in switches:
line = QLineSeries()
line.setPen(QPen(QColor(33, 150, 243), 2))
line.append(idx, 0)
line.append(idx, 1)
self.chart.addSeries(line)
line.attachAxis(self.x_axis)
line.attachAxis(self.y_axis)
self.event_lines.append(line)
for idx in res_changes:
line = QLineSeries()
line.setPen(QPen(QColor(255, 167, 38), 2))
line.append(idx, 0)
line.append(idx, 1)
self.chart.addSeries(line)
line.attachAxis(self.x_axis)
line.attachAxis(self.y_axis)
self.event_lines.append(line)
self.x_axis.setRange(0, n)
self.y_axis.setRange(0, 1)
self.legend_label.setText("<span style='color:#ff4444;'>CPU Spikes</span>: {} | <span style='color:#ff5722;'>GPU Spikes</span>: {} | <span style='color:#2196f3;'>Switches</span>: {} | <span style='color:#ffa726;'>Res Changes</span>: {}".format(len(cpu_spikes), len(gpu_spikes), len(switches), len(res_changes)))
class AnalyticsTab(QWidget):
"""Analytics tab with charts and statistics"""
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
"""Initialize UI components"""
main_layout = QVBoxLayout(self)
# Add notice that violations are disabled
notice_label = QLabel("⚠️ Violation detection is currently disabled. Only object detection statistics will be shown.")
notice_label.setStyleSheet("font-size: 14px; color: #FFA500; font-weight: bold; padding: 10px;")
notice_label.setAlignment(Qt.AlignCenter)
main_layout.addWidget(notice_label)
# Charts section
charts_splitter = QSplitter(Qt.Horizontal)
# Latency chart (top, full width)
self.latency_chart = LatencyChartWidget("Inference Latency Over Time")
main_layout.addWidget(self.latency_chart)
# Left side - Time series chart
self.time_series_chart = TimeSeriesChart("Traffic Over Time")
charts_splitter.addWidget(self.time_series_chart)
# Right side - Detection and violation charts
right_charts = QWidget()
right_layout = QVBoxLayout(right_charts)
self.detection_chart = DetectionPieChart("Detection Classes")
self.violation_chart = ViolationBarChart("Violations by Type")
right_layout.addWidget(self.detection_chart)
right_layout.addWidget(self.violation_chart)
charts_splitter.addWidget(right_charts)
charts_splitter.setSizes([500, 500]) # Equal initial sizes
main_layout.addWidget(charts_splitter)
# Key metrics section
metrics_box = QGroupBox("Key Metrics")
metrics_layout = QHBoxLayout(metrics_box)
# Vehicle metrics
vehicle_metrics = QGroupBox("Traffic")
vehicle_layout = QVBoxLayout(vehicle_metrics)
self.total_vehicles_label = QLabel("Total Vehicles: 0")
self.total_pedestrians_label = QLabel("Total Pedestrians: 0")
vehicle_layout.addWidget(self.total_vehicles_label)
vehicle_layout.addWidget(self.total_pedestrians_label)
metrics_layout.addWidget(vehicle_metrics)
# Violation metrics
violation_metrics = QGroupBox("Violations")
violation_layout = QVBoxLayout(violation_metrics)
self.total_violations_label = QLabel("Total Violations: 0")
self.peak_violation_label = QLabel("Peak Violation Hour: --")
violation_layout.addWidget(self.total_violations_label)
violation_layout.addWidget(self.peak_violation_label)
metrics_layout.addWidget(violation_metrics)
# Performance metrics
performance_metrics = QGroupBox("Performance")
performance_layout = QVBoxLayout(performance_metrics)
self.avg_fps_label = QLabel("Avg FPS: 0")
self.avg_processing_label = QLabel("Avg Processing Time: 0 ms")
performance_layout.addWidget(self.avg_fps_label)
performance_layout.addWidget(self.avg_processing_label)
metrics_layout.addWidget(performance_metrics)
main_layout.addWidget(metrics_box)
# Controls
controls = QHBoxLayout()
self.reset_btn = QPushButton("Reset Statistics")
controls.addWidget(self.reset_btn)
controls.addStretch(1) # Push button to left
main_layout.addLayout(controls)
@Slot(dict)
def update_analytics(self, analytics):
"""
Update analytics display with new data.
Args:
analytics: Dictionary of analytics data
"""
try:
if not analytics:
return
# Update latency chart
try:
if hasattr(self, 'latency_chart') and self.latency_chart is not None:
self.latency_chart.update_data(analytics.get('latency', {}))
except Exception as e:
print(f"[WARNING] Latency chart update failed: {e}")
# Update charts with error handling
try:
if hasattr(self, 'time_series_chart') and self.time_series_chart is not None:
self.time_series_chart.update_data(analytics.get('time_series', {}))
except Exception as e:
print(f"[WARNING] Time series chart update failed: {e}")
try:
if hasattr(self, 'detection_chart') and self.detection_chart is not None:
self.detection_chart.update_data(analytics.get('detection_counts', {}))
except Exception as e:
print(f"[WARNING] Detection chart update failed: {e}")
try:
if hasattr(self, 'violation_chart') and self.violation_chart is not None:
self.violation_chart.update_data(analytics.get('violation_counts', {}))
except Exception as e:
print(f"[WARNING] Violation chart update failed: {e}")
# Update metrics
try:
metrics = analytics.get('metrics', {})
if hasattr(self, 'total_vehicles_label'):
self.total_vehicles_label.setText(f"Total Vehicles: {metrics.get('total_vehicles', 0)}")
if hasattr(self, 'total_pedestrians_label'):
self.total_pedestrians_label.setText(f"Total Pedestrians: {metrics.get('total_pedestrians', 0)}")
if hasattr(self, 'total_violations_label'):
self.total_violations_label.setText(f"Total Violations: {metrics.get('total_violations', 0)}")
peak_hour = metrics.get('peak_violation_hour')
if peak_hour:
peak_text = f"Peak Violation Hour: {peak_hour.get('time', '--')} ({peak_hour.get('violations', 0)})"
else:
peak_text = "Peak Violation Hour: --"
if hasattr(self, 'peak_violation_label'):
self.peak_violation_label.setText(peak_text)
if hasattr(self, 'avg_fps_label'):
self.avg_fps_label.setText(f"Avg FPS: {metrics.get('avg_fps', 0):.1f}")
if hasattr(self, 'avg_processing_label'):
self.avg_processing_label.setText(
f"Avg Processing Time: {metrics.get('avg_processing_time', 0):.1f} ms"
)
except Exception as e:
print(f"[WARNING] Metrics update failed: {e}")
# Update traffic light label with latest color
try:
tl_series = analytics.get('traffic_light_color_series', [])
if tl_series:
latest = tl_series[-1][1]
self.traffic_light_label.setText(f"Traffic Light: {latest.title()}")
else:
self.traffic_light_label.setText("Traffic Light: Unknown")
except Exception as e:
print(f"[WARNING] Traffic light label update failed: {e}")
except Exception as e:
print(f"[ERROR] Analytics update failed: {e}")

View File

@@ -0,0 +1,666 @@
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox,
QSlider, QCheckBox, QPushButton, QGroupBox, QFormLayout,
QSpinBox, QDoubleSpinBox, QTabWidget, QLineEdit, QFileDialog,
QSpacerItem, QSizePolicy
)
from PySide6.QtCore import Qt, Signal, Slot
from PySide6.QtGui import QFont
class ConfigPanel(QWidget):
"""Side panel for application configuration."""
config_changed = Signal(dict) # Emitted when configuration changes are applied
theme_toggled = Signal(bool) # Emitted when theme toggle button is clicked (True = dark)
device_switch_requested = Signal(str)
def __init__(self):
super().__init__()
self.setObjectName("ConfigPanel")
self.setStyleSheet(self._panel_qss())
self.initUI()
self.dark_theme = True # Start with dark theme
def _panel_qss(self):
return """
#ConfigPanel {
background: #181C20;
border-top-left-radius: 18px;
border-bottom-left-radius: 18px;
border: none;
}
QTabWidget::pane {
border-radius: 12px;
background: #232323;
}
QTabBar::tab {
background: #232323;
color: #bbb;
border-radius: 10px 10px 0 0;
padding: 8px 18px;
font-size: 15px;
}
QTabBar::tab:selected {
background: #03DAC5;
color: #181C20;
}
QGroupBox {
border: 1px solid #30343A;
border-radius: 12px;
margin-top: 16px;
background: #232323;
font-weight: bold;
color: #fff;
font-size: 15px;
}
QGroupBox:title {
subcontrol-origin: margin;
left: 12px;
top: 8px;
padding: 0 4px;
background: transparent;
}
QLabel, QCheckBox, QRadioButton {
color: #eee;
font-size: 14px;
}
QLineEdit, QSpinBox, QDoubleSpinBox {
background: #181C20;
border: 1.5px solid #30343A;
border-radius: 8px;
color: #fff;
padding: 6px 10px;
font-size: 14px;
}
QSlider::groove:horizontal {
height: 8px;
background: #30343A;
border-radius: 4px;
}
QSlider::handle:horizontal {
background: #03DAC5;
border-radius: 10px;
width: 20px;
}
QPushButton {
background: #03DAC5;
color: #181C20;
border-radius: 10px;
font-size: 15px;
font-weight: 600;
padding: 8px 18px;
border: none;
}
QPushButton:hover {
background: #018786;
color: #fff;
}
QPushButton:pressed {
background: #03DAC5;
color: #232323;
}
QCheckBox::indicator {
border-radius: 6px;
width: 18px;
height: 18px;
}
QCheckBox::indicator:checked {
background: #03DAC5;
border: 1.5px solid #018786;
}
QCheckBox::indicator:unchecked {
background: #232323;
border: 1.5px solid #30343A;
}
"""
def initUI(self):
"""Initialize UI components"""
layout = QVBoxLayout(self)
layout.setContentsMargins(18, 18, 18, 18)
layout.setSpacing(10)
# Create tab widget for better organization
tabs = QTabWidget()
tabs.setStyleSheet("") # Use panel QSS
# Detection tab
detection_tab = QWidget()
detection_layout = QVBoxLayout(detection_tab)
# Device selection
device_group = QGroupBox("Inference Device")
device_layout = QVBoxLayout(device_group)
self.device_combo = QComboBox()
self.device_combo.addItems(["AUTO", "CPU", "GPU", "MYRIAD", "VPU"])
device_layout.addWidget(self.device_combo)
detection_layout.addWidget(device_group)
# Detection settings
detection_group = QGroupBox("Detection Settings")
detection_form = QFormLayout(detection_group)
self.conf_slider = QSlider(Qt.Horizontal)
self.conf_slider.setRange(10, 100)
self.conf_slider.setValue(50)
self.conf_slider.setTracking(True)
self.conf_slider.valueChanged.connect(self.update_conf_label)
self.conf_label = QLabel("50%")
conf_layout = QHBoxLayout()
conf_layout.addWidget(self.conf_slider)
conf_layout.addWidget(self.conf_label)
self.tracking_checkbox = QCheckBox("Enable")
self.tracking_checkbox.setChecked(True)
model_layout = QHBoxLayout()
self.model_path = QLineEdit()
self.model_path.setReadOnly(True)
self.model_path.setPlaceholderText("Auto-detected")
self.browse_btn = QPushButton("...")
self.browse_btn.setMaximumWidth(30)
self.browse_btn.clicked.connect(self.browse_model)
model_layout.addWidget(self.model_path)
model_layout.addWidget(self.browse_btn)
detection_form.addRow("Confidence Threshold:", conf_layout)
detection_form.addRow("Object Tracking:", self.tracking_checkbox)
detection_form.addRow("Model Path:", model_layout)
detection_layout.addWidget(detection_group)
# Add quick switch buttons for YOLO11n/YOLO11x
quick_switch_layout = QHBoxLayout()
self.cpu_switch_btn = QPushButton("Switch to CPU (YOLO11n)")
self.gpu_switch_btn = QPushButton("Switch to GPU (YOLO11x)")
self.cpu_switch_btn.clicked.connect(lambda: self.quick_switch_device("CPU"))
self.gpu_switch_btn.clicked.connect(lambda: self.quick_switch_device("GPU"))
quick_switch_layout.addWidget(self.cpu_switch_btn)
quick_switch_layout.addWidget(self.gpu_switch_btn)
detection_layout.addLayout(quick_switch_layout)
# --- Current Model Info Section (PREMIUM FORMAT) ---
model_info_group = QGroupBox()
model_info_group.setTitle("")
model_info_group.setStyleSheet("""
QGroupBox {
border: 1.5px solid #03DAC5;
border-radius: 12px;
margin-top: 16px;
background: #181C20;
font-weight: bold;
color: #03DAC5;
font-size: 16px;
}
""")
model_info_layout = QVBoxLayout(model_info_group)
model_info_layout.setContentsMargins(16, 10, 16, 10)
# Title
title = QLabel("Current Model")
title.setStyleSheet("font-size: 17px; font-weight: bold; color: #03DAC5; margin-bottom: 8px;")
model_info_layout.addWidget(title)
# Info rows
row_style = "font-size: 15px; color: #fff; font-family: 'Consolas', 'SF Mono', 'monospace'; padding: 2px 0;"
row_widget = QWidget()
row_layout = QVBoxLayout(row_widget)
row_layout.setContentsMargins(0, 0, 0, 0)
row_layout.setSpacing(2)
# Model
model_row = QHBoxLayout()
model_label = QLabel("Model:")
model_label.setStyleSheet(row_style + "font-weight: 600; color: #80cbc4;")
self.current_model_label = QLabel("-")
self.current_model_label.setStyleSheet(row_style)
model_row.addWidget(model_label)
model_row.addWidget(self.current_model_label, 1)
row_layout.addLayout(model_row)
# Device
device_row = QHBoxLayout()
device_label = QLabel("Device:")
device_label.setStyleSheet(row_style + "font-weight: 600; color: #80cbc4;")
self.current_device_label = QLabel("-")
self.current_device_label.setStyleSheet(row_style)
device_row.addWidget(device_label)
device_row.addWidget(self.current_device_label, 1)
row_layout.addLayout(device_row)
# Recommended For
rec_row = QHBoxLayout()
rec_label = QLabel("Recommended For:")
rec_label.setStyleSheet(row_style + "font-weight: 600; color: #80cbc4;")
self.model_recommendation_label = QLabel("")
self.model_recommendation_label.setStyleSheet(row_style)
rec_row.addWidget(rec_label)
rec_row.addWidget(self.model_recommendation_label, 1)
row_layout.addLayout(rec_row)
model_info_layout.addWidget(row_widget)
model_info_layout.addStretch(1)
detection_layout.addWidget(model_info_group)
# --- OpenVINO Devices Info Section ---
devices_info_group = QGroupBox()
devices_info_group.setTitle("")
devices_info_group.setStyleSheet("""
QGroupBox {
border: 1.5px solid #80cbc4;
border-radius: 12px;
margin-top: 16px;
background: #181C20;
font-weight: bold;
color: #80cbc4;
font-size: 16px;
}
""")
devices_info_layout = QVBoxLayout(devices_info_group)
devices_info_layout.setContentsMargins(16, 10, 16, 10)
devices_title = QLabel("Available OpenVINO Devices")
devices_title.setStyleSheet("font-size: 16px; font-weight: bold; color: #80cbc4; margin-bottom: 8px;")
devices_info_layout.addWidget(devices_title)
self.devices_info_text = QLabel("Yolov11n and Yolov11x models are optimized for CPU and GPU respectively.<br>")
self.devices_info_text.setStyleSheet("font-size: 14px; color: #fff; font-family: 'Consolas', 'SF Mono', 'monospace';")
self.devices_info_text.setWordWrap(True)
self.devices_info_text.setTextFormat(Qt.RichText)
self.devices_info_text.setObjectName("devices_info_text")
devices_info_layout.addWidget(self.devices_info_text)
devices_info_layout.addStretch(1)
detection_layout.addWidget(devices_info_group)
display_tab = QWidget()
display_layout = QVBoxLayout(display_tab)
# Display options
display_group = QGroupBox("Display Options")
display_form = QFormLayout(display_group)
self.labels_checkbox = QCheckBox()
self.labels_checkbox.setChecked(True)
self.confidence_checkbox = QCheckBox()
self.confidence_checkbox.setChecked(True)
self.perf_checkbox = QCheckBox()
self.perf_checkbox.setChecked(True)
self.max_width = QSpinBox()
self.max_width.setRange(320, 4096)
self.max_width.setValue(800)
self.max_width.setSingleStep(10)
self.max_width.setSuffix(" px")
display_form.addRow("Show Labels:", self.labels_checkbox)
display_form.addRow("Show Confidence:", self.confidence_checkbox)
display_form.addRow("Show Performance:", self.perf_checkbox)
display_form.addRow("Max Display Width:", self.max_width)
display_layout.addWidget(display_group)
# Analytics Group
analytics_group = QGroupBox("Analytics Settings")
analytics_form = QFormLayout(analytics_group)
self.charts_checkbox = QCheckBox()
self.charts_checkbox.setChecked(True)
self.history_spinbox = QSpinBox()
self.history_spinbox.setRange(10, 10000)
self.history_spinbox.setValue(1000)
self.history_spinbox.setSingleStep(100)
self.history_spinbox.setSuffix(" frames")
analytics_form.addRow("Enable Live Charts:", self.charts_checkbox)
analytics_form.addRow("History Length:", self.history_spinbox)
display_layout.addWidget(analytics_group)
# Violation tab
violation_tab = QWidget()
violation_layout = QVBoxLayout(violation_tab)
# Violation settings
violation_group = QGroupBox("Violation Detection")
violation_form = QFormLayout(violation_group)
self.red_light_grace = QDoubleSpinBox()
self.red_light_grace.setRange(0.1, 5.0)
self.red_light_grace.setValue(2.0)
self.red_light_grace.setSingleStep(0.1)
self.red_light_grace.setSuffix(" sec")
self.stop_sign_duration = QDoubleSpinBox()
self.stop_sign_duration.setRange(0.5, 5.0)
self.stop_sign_duration.setValue(2.0)
self.stop_sign_duration.setSingleStep(0.1)
self.stop_sign_duration.setSuffix(" sec")
self.speed_tolerance = QSpinBox()
self.speed_tolerance.setRange(0, 20)
self.speed_tolerance.setValue(5)
self.speed_tolerance.setSingleStep(1)
self.speed_tolerance.setSuffix(" km/h")
violation_form.addRow("Red Light Grace:", self.red_light_grace)
violation_form.addRow("Stop Sign Duration:", self.stop_sign_duration)
violation_form.addRow("Speed Tolerance:", self.speed_tolerance)
self.enable_red_light = QCheckBox("Enabled")
self.enable_red_light.setChecked(True)
self.enable_stop_sign = QCheckBox("Enabled")
self.enable_stop_sign.setChecked(True)
self.enable_speed = QCheckBox("Enabled")
self.enable_speed.setChecked(True)
self.enable_lane = QCheckBox("Enabled")
self.enable_lane.setChecked(True)
violation_form.addRow("Red Light Detection:", self.enable_red_light)
violation_form.addRow("Stop Sign Detection:", self.enable_stop_sign)
violation_form.addRow("Speed Detection:", self.enable_speed)
violation_form.addRow("Lane Detection:", self.enable_lane)
violation_layout.addWidget(violation_group)
# Add all tabs
tabs.addTab(detection_tab, "Detection")
tabs.addTab(display_tab, "Display")
tabs.addTab(violation_tab, "Violations")
layout.addWidget(tabs)
# Theme toggle
self.theme_toggle = QPushButton("🌙 Dark Theme")
self.theme_toggle.setFixedHeight(36)
self.theme_toggle.setStyleSheet("margin-top: 8px;")
self.theme_toggle.clicked.connect(self.toggle_theme)
layout.addWidget(self.theme_toggle)
# Spacer to push buttons to bottom
layout.addItem(QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding))
# Control buttons (fixed at bottom)
btns = QHBoxLayout()
self.apply_btn = QPushButton("Apply")
self.apply_btn.setFixedHeight(32)
self.apply_btn.clicked.connect(self.apply_config)
self.reset_btn = QPushButton("Reset")
self.reset_btn.setFixedHeight(32)
self.reset_btn.clicked.connect(self.reset_config)
btns.addWidget(self.apply_btn)
btns.addWidget(self.reset_btn)
layout.addLayout(btns)
layout.addStretch(1) # Push everything to the top
# Set tooltips for major controls
self.device_combo.setToolTip("Select inference device (CPU, GPU, etc.)")
self.cpu_switch_btn.setToolTip("Switch to CPU-optimized YOLO11n model")
self.gpu_switch_btn.setToolTip("Switch to GPU-optimized YOLO11x model")
self.conf_slider.setToolTip("Set detection confidence threshold")
self.tracking_checkbox.setToolTip("Enable or disable object tracking")
self.model_path.setToolTip("Path to the detection model")
self.browse_btn.setToolTip("Browse for a model file")
self.labels_checkbox.setToolTip("Show/hide detection labels on video")
self.confidence_checkbox.setToolTip("Show/hide confidence scores on video")
self.perf_checkbox.setToolTip("Show/hide performance overlay")
self.max_width.setToolTip("Maximum display width for video")
self.charts_checkbox.setToolTip("Enable/disable live analytics charts")
self.history_spinbox.setToolTip("Number of frames to keep in analytics history")
self.red_light_grace.setToolTip("Grace period for red light violation (seconds)")
self.stop_sign_duration.setToolTip("Stop sign violation duration (seconds)")
self.speed_tolerance.setToolTip("Speed tolerance for speed violation (km/h)")
self.enable_red_light.setToolTip("Enable/disable red light violation detection")
self.enable_stop_sign.setToolTip("Enable/disable stop sign violation detection")
self.enable_speed.setToolTip("Enable/disable speed violation detection")
self.enable_lane.setToolTip("Enable/disable lane violation detection")
self.theme_toggle.setToolTip("Toggle between dark and light theme")
self.apply_btn.setToolTip("Apply all changes")
self.reset_btn.setToolTip("Reset all settings to default")
@Slot(int)
def update_conf_label(self, value):
"""Update confidence threshold label"""
self.conf_label.setText(f"{value}%")
@Slot()
def browse_model(self):
"""Browse for model file"""
file_path, _ = QFileDialog.getOpenFileName(
self,
"Select Model File",
"",
"OpenVINO Models (*.xml);;PyTorch Models (*.pt);;All Files (*)"
)
if file_path:
self.model_path.setText(file_path)
@Slot()
def toggle_theme(self):
"""Toggle between light and dark theme"""
self.dark_theme = not self.dark_theme
if self.dark_theme:
self.theme_toggle.setText("🌙 Dark Theme")
else:
self.theme_toggle.setText("☀️ Light Theme")
self.theme_toggled.emit(self.dark_theme)
@Slot()
def apply_config(self):
"""Apply configuration changes"""
config = self.get_config()
self.config_changed.emit(config)
@Slot()
def reset_config(self):
"""Reset configuration to defaults"""
self.device_combo.setCurrentText("AUTO")
self.conf_slider.setValue(50)
self.tracking_checkbox.setChecked(True)
self.labels_checkbox.setChecked(True)
self.confidence_checkbox.setChecked(True)
self.perf_checkbox.setChecked(True)
self.max_width.setValue(800)
self.red_light_grace.setValue(2.0)
self.stop_sign_duration.setValue(2.0)
self.speed_tolerance.setValue(5)
self.enable_red_light.setChecked(True)
self.enable_stop_sign.setChecked(True)
self.enable_speed.setChecked(True)
self.enable_lane.setChecked(True)
self.model_path.setText("")
self.apply_config()
def quick_switch_device(self, device: str):
index = self.device_combo.findText(device)
if index >= 0:
self.device_combo.setCurrentIndex(index)
self.device_switch_requested.emit(device)
self.apply_config()
def update_model_info(self, model_info: dict):
if not model_info:
self.current_model_label.setText("No model loaded")
self.current_device_label.setText("None")
self.model_recommendation_label.setText("None")
return
model_name = model_info.get("model_name", "Unknown")
device = model_info.get("device", "Unknown")
recommended_for = model_info.get("recommended_for", "Unknown")
self.current_model_label.setText(model_name)
self.current_device_label.setText(device)
self.model_recommendation_label.setText(recommended_for)
if device == "CPU":
self.cpu_switch_btn.setEnabled(False)
self.cpu_switch_btn.setText("✓ CPU Active (YOLO11n)")
self.gpu_switch_btn.setEnabled(True)
self.gpu_switch_btn.setText("Switch to GPU (YOLO11x)")
elif device == "GPU":
self.cpu_switch_btn.setEnabled(True)
self.cpu_switch_btn.setText("Switch to CPU (YOLO11n)")
self.gpu_switch_btn.setEnabled(False)
self.gpu_switch_btn.setText("✓ GPU Active (YOLO11x)")
else:
self.cpu_switch_btn.setEnabled(True)
self.cpu_switch_btn.setText("Switch to CPU (YOLO11n)")
self.gpu_switch_btn.setEnabled(True)
self.gpu_switch_btn.setText("Switch to GPU (YOLO11x)")
@Slot(object, object)
def update_live_stats(self, fps, inference_time):
"""Update FPS and inference time labels in the settings panel."""
if fps is not None:
self.fps_label.setText(f"FPS: {fps:.1f}")
else:
self.fps_label.setText("FPS: --")
if inference_time is not None:
self.infer_label.setText(f"Inference: {inference_time:.1f} ms")
else:
self.infer_label.setText("Inference: -- ms")
@Slot(object, object)
def set_video_stats(self, stats):
"""Update FPS and inference time labels in the settings panel from stats dict."""
fps = stats.get('fps', None)
inference_time = None
if 'detection_time_ms' in stats:
inference_time = float(stats['detection_time_ms'])
elif 'detection_time' in stats:
inference_time = float(stats['detection_time'])
self.update_live_stats(fps, inference_time)
def get_config(self):
"""
Get current configuration from UI.
Returns:
Configuration dictionary
"""
return {
'detection': {
'device': self.device_combo.currentText(),
'confidence_threshold': self.conf_slider.value() / 100.0,
'enable_tracking': self.tracking_checkbox.isChecked(),
'model_path': self.model_path.text() if self.model_path.text() else None
},
'display': {
'show_labels': self.labels_checkbox.isChecked(),
'show_confidence': self.confidence_checkbox.isChecked(),
'show_performance': self.perf_checkbox.isChecked(),
'max_display_width': self.max_width.value()
},
'violations': {
'red_light_grace_period': self.red_light_grace.value(),
'stop_sign_duration': self.stop_sign_duration.value(),
'speed_tolerance': self.speed_tolerance.value(),
'enable_red_light': self.enable_red_light.isChecked(),
'enable_stop_sign': self.enable_stop_sign.isChecked(),
'enable_speed': self.enable_speed.isChecked(),
'enable_lane': self.enable_lane.isChecked()
},
'analytics': {
'enable_charts': self.charts_checkbox.isChecked(),
'history_length': self.history_spinbox.value()
}
}
def set_config(self, config):
"""
Set configuration in UI.
Args:
config: Configuration dictionary
"""
if not config:
return
# Detection settings
detection = config.get('detection', {})
if 'device' in detection:
index = self.device_combo.findText(detection['device'])
if index >= 0:
self.device_combo.setCurrentIndex(index)
if 'confidence_threshold' in detection:
self.conf_slider.setValue(int(detection['confidence_threshold'] * 100))
if 'enable_tracking' in detection:
self.tracking_checkbox.setChecked(detection['enable_tracking'])
if 'model_path' in detection and detection['model_path']:
self.model_path.setText(detection['model_path'])
# Display settings
display = config.get('display', {})
if 'show_labels' in display:
self.labels_checkbox.setChecked(display['show_labels'])
if 'show_confidence' in display:
self.confidence_checkbox.setChecked(display['show_confidence'])
if 'show_performance' in display:
self.perf_checkbox.setChecked(display['show_performance'])
if 'max_display_width' in display:
self.max_width.setValue(display['max_display_width'])
# Violation settings
violations = config.get('violations', {})
if 'red_light_grace_period' in violations:
self.red_light_grace.setValue(violations['red_light_grace_period'])
if 'stop_sign_duration' in violations:
self.stop_sign_duration.setValue(violations['stop_sign_duration'])
if 'speed_tolerance' in violations:
self.speed_tolerance.setValue(violations['speed_tolerance'])
if 'enable_red_light' in violations:
self.enable_red_light.setChecked(violations['enable_red_light'])
if 'enable_stop_sign' in violations:
self.enable_stop_sign.setChecked(violations['enable_stop_sign'])
if 'enable_speed' in violations:
self.enable_speed.setChecked(violations['enable_speed'])
if 'enable_lane' in violations:
self.enable_lane.setChecked(violations['enable_lane'])
# Analytics settings
analytics = config.get('analytics', {})
if 'enable_charts' in analytics:
self.charts_checkbox.setChecked(analytics['enable_charts'])
if 'history_length' in analytics:
self.history_spinbox.setValue(analytics['history_length'])
@Slot(object)
def update_devices_info(self, device_info: dict):
"""
Update the OpenVINO devices info section with the given device info dict.
"""
print(f"[UI] update_devices_info called with: {device_info}", flush=True) # DEBUG
if not device_info:
self.devices_info_text.setText("<span style='color:#ffb300;'>No OpenVINO device info received.<br>Check if OpenVINO is installed and the backend emits device_info_ready.</span>")
return
if 'error' in device_info:
self.devices_info_text.setText(f"<span style='color:#ff5370;'>Error: {device_info['error']}</span>")
return
text = ""
for device, props in device_info.items():
text += f"<b>{device}</b><br>"
if isinstance(props, dict) and props:
for k, v in props.items():
text += f"&nbsp;&nbsp;<span style='color:#b2dfdb;'>{k}</span>: <span style='color:#fff'>{v}</span><br>"
else:
text += "&nbsp;&nbsp;<span style='color:#888'>No properties</span><br>"
text += "<br>"
self.devices_info_text.setText(f"<div style='font-size:13px;'>{text}</div>")
self.devices_info_text.repaint() # Force repaint in case of async update

View File

@@ -0,0 +1,208 @@
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QLabel, QSizePolicy,
QGraphicsView, QGraphicsScene
)
from PySide6.QtCore import Qt, Signal, QSize
from PySide6.QtGui import QPixmap, QImage, QPainter
import cv2
import numpy as np
import time
class SimpleLiveDisplay(QWidget):
"""Enhanced implementation for video display using QGraphicsView"""
video_dropped = Signal(str) # For drag and drop compatibility
def __init__(self):
super().__init__()
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
# Create QGraphicsView and QGraphicsScene
self.graphics_view = QGraphicsView()
self.graphics_scene = QGraphicsScene()
self.graphics_view.setScene(self.graphics_scene)
self.graphics_view.setMinimumSize(640, 480)
self.graphics_view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.graphics_view.setStyleSheet("background-color: black;")
self.graphics_view.setRenderHint(QPainter.Antialiasing)
self.graphics_view.setRenderHint(QPainter.SmoothPixmapTransform)
# Create backup label (in case QGraphicsView doesn't work)
self.display_label = QLabel()
self.display_label.setAlignment(Qt.AlignCenter)
self.display_label.setMinimumSize(640, 480)
self.display_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.display_label.setStyleSheet("background-color: black;")
# Track frame update times
self.last_update = time.time()
self.frame_count = 0
self.fps = 0.0
# Set up drag and drop
self.setAcceptDrops(True)
# Add QGraphicsView to layout (primary display)
self.layout.addWidget(self.graphics_view)
# Don't add label to layout, we'll only use it as fallback if needed
def update_frame(self, pixmap, overlay_states=None):
"""Update the display with a new frame, using overlay_states to control overlays"""
if overlay_states is None:
overlay_states = {
'show_vehicles': True,
'show_ids': True,
'show_red_light': True,
'show_violation': True,
}
if pixmap and not pixmap.isNull():
print(f"DEBUG: SimpleLiveDisplay updating with pixmap {pixmap.width()}x{pixmap.height()}")
# Here you would use overlay_states to control what is drawn
# For example, in your actual drawing logic:
# if overlay_states['show_vehicles']:
# draw detection boxes
# if overlay_states['show_ids']:
# draw IDs
# if overlay_states['show_red_light']:
# draw traffic light color
# if overlay_states['show_violation']:
# draw violation line
try:
self.graphics_scene.clear()
self.graphics_scene.addPixmap(pixmap)
self.graphics_view.fitInView(self.graphics_scene.itemsBoundingRect(), Qt.KeepAspectRatio)
self.graphics_view.update()
self.graphics_view.viewport().update()
print("DEBUG: SimpleLiveDisplay - pixmap displayed successfully in QGraphicsView")
except Exception as e:
print(f"ERROR in QGraphicsView display: {e}, falling back to QLabel")
try:
scaled_pixmap = pixmap.scaled(
self.display_label.width() or pixmap.width(),
self.display_label.height() or pixmap.height(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
self.display_label.setPixmap(scaled_pixmap)
self.display_label.update()
except Exception as e2:
print(f"ERROR in QLabel fallback: {e2}")
import traceback
traceback.print_exc()
else:
print("DEBUG: SimpleLiveDisplay received null or invalid pixmap")
def resizeEvent(self, event):
"""Handle resize events"""
super().resizeEvent(event)
# If we have content in the scene, resize it to fit
if not self.graphics_scene.items():
return
self.graphics_view.fitInView(self.graphics_scene.itemsBoundingRect(), Qt.KeepAspectRatio)
def reset_display(self):
"""Reset display to black"""
blank = QPixmap(self.width(), self.height())
blank.fill(Qt.black)
self.update_frame(blank)
def dragEnterEvent(self, event):
"""Handle drag enter events"""
if event.mimeData().hasUrls():
url = event.mimeData().urls()[0].toLocalFile()
if url.lower().endswith(('.mp4', '.avi', '.mov', '.mkv', '.webm')):
event.acceptProposedAction()
def dropEvent(self, event):
"""Handle drop events"""
if event.mimeData().hasUrls():
url = event.mimeData().urls()[0].toLocalFile()
if url.lower().endswith(('.mp4', '.avi', '.mov', '.mkv', '.webm')):
self.video_dropped.emit(url)
def display_frame(self, frame: np.ndarray):
"""Display a NumPy OpenCV frame directly (converts to QPixmap and displays)"""
# Check for frame validity
if frame is None:
print("⚠️ Empty frame received")
return
# Calculate FPS
now = time.time()
time_diff = now - self.last_update
self.frame_count += 1
if time_diff >= 1.0:
self.fps = self.frame_count / time_diff
print(f"🎬 Display FPS: {self.fps:.2f}")
self.frame_count = 0
self.last_update = now
# Print debug info about the frame
print(f"🟢 display_frame: frame shape={getattr(frame, 'shape', None)}, dtype={getattr(frame, 'dtype', None)}")
print(f"💾 Frame memory address: {hex(id(frame))}")
try:
print("💻 Processing frame for display...")
# Make a copy of the frame to ensure we're not using memory that might be released
frame_copy = frame.copy()
# Convert BGR to RGB (OpenCV uses BGR, Qt uses RGB)
rgb_frame = cv2.cvtColor(frame_copy, cv2.COLOR_BGR2RGB)
# Force continuous array for QImage
is_contiguous = rgb_frame.flags.c_contiguous
print(f"🔄 RGB frame is contiguous: {is_contiguous}")
if not is_contiguous:
print("⚙️ Making frame contiguous...")
rgb_frame = np.ascontiguousarray(rgb_frame)
# Get dimensions
h, w, ch = rgb_frame.shape
bytes_per_line = ch * w
print(f"📏 Frame dimensions: {w}x{h}, channels: {ch}, bytes_per_line: {bytes_per_line}")
# Create QImage - use .copy() to ensure Qt owns the data
qt_image = QImage(rgb_frame.data, w, h, bytes_per_line, QImage.Format_RGB888).copy()
if qt_image.isNull():
print("⚠️ Failed to create QImage")
return
# Create QPixmap and update display
pixmap = QPixmap.fromImage(qt_image)
print(f"📊 Created pixmap: {pixmap.width()}x{pixmap.height()}, isNull: {pixmap.isNull()}") # Method 1: Use graphics scene (preferred)
try:
self.graphics_scene.clear()
self.graphics_scene.addPixmap(pixmap)
self.graphics_view.fitInView(self.graphics_scene.itemsBoundingRect(), Qt.KeepAspectRatio)
self.graphics_view.update()
self.graphics_view.viewport().update()
# Draw simple FPS counter on the view
fps_text = f"Display: {self.fps:.1f} FPS"
self.graphics_scene.addText(fps_text)
print("✅ Frame displayed in graphics view")
except Exception as e:
print(f"⚠️ QGraphicsView error: {e}, using QLabel fallback")
# Method 2: Fall back to QLabel
if self.display_label.parent() is None:
self.layout.removeWidget(self.graphics_view)
self.graphics_view.hide()
self.layout.addWidget(self.display_label)
self.display_label.show()
# Set pixmap on the label
self.display_label.setPixmap(pixmap)
self.display_label.setScaledContents(True)
print("✅ Frame displayed in label (fallback)")
except Exception as e:
print(f"❌ Critical error in display_frame: {e}")
import traceback
traceback.print_exc()

View File

@@ -0,0 +1,360 @@
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QFileDialog,
QPlainTextEdit, QGroupBox, QLabel, QComboBox, QCheckBox,
QTableWidget, QTableWidgetItem, QFormLayout, QLineEdit,
QDateTimeEdit, QSpinBox, QTabWidget, QStyle
)
from PySide6.QtCore import Qt, Slot, QDateTime
from PySide6.QtGui import QFont
class ConfigSection(QGroupBox):
"""Configuration editor section"""
def __init__(self, title):
super().__init__(title)
self.layout = QVBoxLayout(self)
class ExportTab(QWidget):
"""Tab for exporting data and managing configuration."""
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
"""Initialize UI components"""
main_layout = QVBoxLayout(self)
# Create tab widget for organizing export and config sections
tab_widget = QTabWidget()
# Tab 1: Export Data
export_tab = QWidget()
export_layout = QVBoxLayout(export_tab)
# Export options
export_options = QGroupBox("Export Options")
options_layout = QFormLayout(export_options)
self.export_format_combo = QComboBox()
self.export_format_combo.addItems(["CSV", "JSON", "Excel", "PDF Report"])
self.export_data_combo = QComboBox()
self.export_data_combo.addItems([
"All Data",
"Detections Only",
"Violations Only",
"Analytics Summary"
])
# Time range
time_layout = QHBoxLayout()
self.export_range_check = QCheckBox("Time Range:")
self.export_range_check.setChecked(False)
self.export_start_time = QDateTimeEdit(QDateTime.currentDateTime().addDays(-1))
self.export_start_time.setEnabled(False)
self.export_end_time = QDateTimeEdit(QDateTime.currentDateTime())
self.export_end_time.setEnabled(False)
self.export_range_check.toggled.connect(self.export_start_time.setEnabled)
self.export_range_check.toggled.connect(self.export_end_time.setEnabled)
time_layout.addWidget(self.export_range_check)
time_layout.addWidget(self.export_start_time)
time_layout.addWidget(QLabel("to"))
time_layout.addWidget(self.export_end_time)
options_layout.addRow("Export Format:", self.export_format_combo)
options_layout.addRow("Data to Export:", self.export_data_combo)
options_layout.addRow(time_layout)
# Include options
include_layout = QHBoxLayout()
self.include_images_check = QCheckBox("Include Images")
self.include_images_check.setChecked(True)
self.include_analytics_check = QCheckBox("Include Analytics")
self.include_analytics_check.setChecked(True)
include_layout.addWidget(self.include_images_check)
include_layout.addWidget(self.include_analytics_check)
options_layout.addRow("Include:", include_layout)
export_layout.addWidget(export_options)
# Export preview
preview_box = QGroupBox("Export Preview")
preview_layout = QVBoxLayout(preview_box)
self.export_preview = QTableWidget(5, 3)
self.export_preview.setHorizontalHeaderLabels(["Type", "Count", "Details"])
self.export_preview.setAlternatingRowColors(True)
self.export_preview.setEditTriggers(QTableWidget.NoEditTriggers)
# Initialize table items with default values
self.export_preview.setItem(0, 0, QTableWidgetItem("Vehicles"))
self.export_preview.setItem(0, 1, QTableWidgetItem("0"))
self.export_preview.setItem(0, 2, QTableWidgetItem("Cars, trucks, buses"))
self.export_preview.setItem(1, 0, QTableWidgetItem("Pedestrians"))
self.export_preview.setItem(1, 1, QTableWidgetItem("0"))
self.export_preview.setItem(1, 2, QTableWidgetItem("People detected"))
self.export_preview.setItem(2, 0, QTableWidgetItem("Red Light Violations"))
self.export_preview.setItem(2, 1, QTableWidgetItem("0"))
self.export_preview.setItem(2, 2, QTableWidgetItem("Vehicles running red lights"))
self.export_preview.setItem(3, 0, QTableWidgetItem("Stop Sign Violations"))
self.export_preview.setItem(3, 1, QTableWidgetItem("0"))
self.export_preview.setItem(3, 2, QTableWidgetItem("Vehicles ignoring stop signs"))
self.export_preview.setItem(4, 0, QTableWidgetItem("Speed Violations"))
self.export_preview.setItem(4, 1, QTableWidgetItem("0"))
self.export_preview.setItem(4, 2, QTableWidgetItem("Vehicles exceeding speed limits"))
preview_layout.addWidget(self.export_preview)
export_layout.addWidget(preview_box)
# Export buttons
export_buttons = QHBoxLayout()
self.export_btn = QPushButton("Export Data")
self.export_btn.setIcon(self.style().standardIcon(QStyle.SP_DialogSaveButton))
self.clear_export_btn = QPushButton("Clear Data")
export_buttons.addWidget(self.export_btn)
export_buttons.addWidget(self.clear_export_btn)
export_layout.addLayout(export_buttons)
tab_widget.addTab(export_tab, "Export Data")
# Tab 2: Configuration
config_tab = QWidget()
config_layout = QVBoxLayout(config_tab)
# Detection configuration
detection_config = ConfigSection("Detection Configuration")
detection_form = QFormLayout()
self.conf_threshold = QSpinBox()
self.conf_threshold.setRange(1, 100)
self.conf_threshold.setValue(50)
self.conf_threshold.setSuffix("%")
self.enable_tracking = QCheckBox()
self.enable_tracking.setChecked(True)
self.model_path = QLineEdit()
self.model_path.setPlaceholderText("Path to model file")
self.browse_model_btn = QPushButton("Browse...")
model_layout = QHBoxLayout()
model_layout.addWidget(self.model_path)
model_layout.addWidget(self.browse_model_btn)
detection_form.addRow("Confidence Threshold:", self.conf_threshold)
detection_form.addRow("Enable Tracking:", self.enable_tracking)
detection_form.addRow("Model Path:", model_layout)
detection_config.layout.addLayout(detection_form)
# Violation configuration
violation_config = ConfigSection("Violation Configuration")
violation_form = QFormLayout()
self.red_light_grace = QSpinBox()
self.red_light_grace.setRange(0, 10)
self.red_light_grace.setValue(2)
self.red_light_grace.setSuffix(" sec")
self.stop_sign_duration = QSpinBox()
self.stop_sign_duration.setRange(0, 10)
self.stop_sign_duration.setValue(2)
self.stop_sign_duration.setSuffix(" sec")
self.speed_tolerance = QSpinBox()
self.speed_tolerance.setRange(0, 20)
self.speed_tolerance.setValue(5)
self.speed_tolerance.setSuffix(" km/h")
violation_form.addRow("Red Light Grace Period:", self.red_light_grace)
violation_form.addRow("Stop Sign Duration:", self.stop_sign_duration)
violation_form.addRow("Speed Tolerance:", self.speed_tolerance)
violation_config.layout.addLayout(violation_form)
# Display configuration
display_config = ConfigSection("Display Configuration")
display_form = QFormLayout()
self.show_labels = QCheckBox()
self.show_labels.setChecked(True)
self.show_confidence = QCheckBox()
self.show_confidence.setChecked(True)
self.max_display_width = QSpinBox()
self.max_display_width.setRange(320, 4096)
self.max_display_width.setValue(800)
self.max_display_width.setSingleStep(10)
self.max_display_width.setSuffix(" px")
display_form.addRow("Show Labels:", self.show_labels)
display_form.addRow("Show Confidence:", self.show_confidence)
display_form.addRow("Max Display Width:", self.max_display_width)
display_config.layout.addLayout(display_form)
# Add config sections
config_layout.addWidget(detection_config)
config_layout.addWidget(violation_config)
config_layout.addWidget(display_config)
# Config buttons
config_buttons = QHBoxLayout()
self.save_config_btn = QPushButton("Save Configuration")
self.save_config_btn.setIcon(self.style().standardIcon(QStyle.SP_DialogSaveButton))
self.reload_config_btn = QPushButton("Reload Configuration")
self.reload_config_btn.setIcon(self.style().standardIcon(QStyle.SP_BrowserReload))
self.reset_btn = QPushButton("Reset Defaults")
self.reset_btn.setIcon(self.style().standardIcon(QStyle.SP_DialogResetButton))
config_buttons.addWidget(self.save_config_btn)
config_buttons.addWidget(self.reload_config_btn)
config_buttons.addWidget(self.reset_btn)
config_layout.addLayout(config_buttons)
# Raw config editor
raw_config = QGroupBox("Raw Configuration (JSON)")
raw_layout = QVBoxLayout(raw_config)
self.config_editor = QPlainTextEdit()
self.config_editor.setFont(QFont("Consolas", 10))
raw_layout.addWidget(self.config_editor)
config_layout.addWidget(raw_config)
tab_widget.addTab(config_tab, "Configuration")
main_layout.addWidget(tab_widget)
@Slot()
def browse_model_path(self):
"""Browse for model file"""
file_path, _ = QFileDialog.getOpenFileName(
self,
"Select Model File",
"",
"Model Files (*.xml *.bin *.pt *.pth);;All Files (*)"
)
if file_path:
self.model_path.setText(file_path)
@Slot(dict)
def update_export_preview(self, analytics):
"""
Update export preview with analytics data.
Args:
analytics: Dictionary of analytics data
"""
if not analytics:
return
# Update detection counts
detection_counts = analytics.get('detection_counts', {})
vehicle_count = sum([
detection_counts.get('car', 0),
detection_counts.get('truck', 0),
detection_counts.get('bus', 0),
detection_counts.get('motorcycle', 0)
])
pedestrian_count = detection_counts.get('person', 0)
# Update violation counts
violation_counts = analytics.get('violation_counts', {})
red_light_count = violation_counts.get('red_light_violation', 0)
stop_sign_count = violation_counts.get('stop_sign_violation', 0)
speed_count = violation_counts.get('speed_violation', 0)
# Update table - create items if they don't exist
item_data = [
(0, "Vehicles", vehicle_count, "Cars, trucks, buses"),
(1, "Pedestrians", pedestrian_count, "People detected"),
(2, "Red Light Violations", red_light_count, "Vehicles running red lights"),
(3, "Stop Sign Violations", stop_sign_count, "Vehicles ignoring stop signs"),
(4, "Speed Violations", speed_count, "Vehicles exceeding speed limits")
]
for row, label, count, details in item_data:
# Check and create Type column item
if self.export_preview.item(row, 0) is None:
self.export_preview.setItem(row, 0, QTableWidgetItem(label))
# Check and create or update Count column item
if self.export_preview.item(row, 1) is None:
self.export_preview.setItem(row, 1, QTableWidgetItem(str(count)))
else:
self.export_preview.item(row, 1).setText(str(count))
# Check and create Details column item
if self.export_preview.item(row, 2) is None:
self.export_preview.setItem(row, 2, QTableWidgetItem(details))
@Slot(dict)
def update_config_display(self, config):
"""
Update configuration display.
Args:
config: Configuration dictionary
"""
if not config:
return
# Convert to JSON for display
import json
self.config_editor.setPlainText(
json.dumps(config, indent=2)
)
# Update form fields
detection_config = config.get('detection', {})
self.conf_threshold.setValue(int(detection_config.get('confidence_threshold', 0.5) * 100))
self.enable_tracking.setChecked(detection_config.get('enable_tracking', True))
if detection_config.get('model_path'):
self.model_path.setText(detection_config.get('model_path'))
violation_config = config.get('violations', {})
self.red_light_grace.setValue(violation_config.get('red_light_grace_period', 2))
self.stop_sign_duration.setValue(violation_config.get('stop_sign_duration', 2))
self.speed_tolerance.setValue(violation_config.get('speed_tolerance', 5))
display_config = config.get('display', {})
self.show_labels.setChecked(display_config.get('show_labels', True))
self.show_confidence.setChecked(display_config.get('show_confidence', True))
self.max_display_width.setValue(display_config.get('max_display_width', 800))
def get_config_from_ui(self):
"""
Get configuration from UI fields.
Returns:
Configuration dictionary
"""
config = {
'detection': {
'confidence_threshold': self.conf_threshold.value() / 100.0,
'enable_tracking': self.enable_tracking.isChecked(),
'model_path': self.model_path.text() if self.model_path.text() else None
},
'violations': {
'red_light_grace_period': self.red_light_grace.value(),
'stop_sign_duration': self.stop_sign_duration.value(),
'speed_tolerance': self.speed_tolerance.value()
},
'display': {
'max_display_width': self.max_display_width.value(),
'show_confidence': self.show_confidence.isChecked(),
'show_labels': self.show_labels.isChecked()
}
}
return config

View File

@@ -0,0 +1,361 @@
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QFileDialog, QComboBox, QGroupBox, QToolButton, QMessageBox
)
from PySide6.QtCore import Qt, Signal, QSize, Slot, QTimer
from PySide6.QtGui import QPixmap, QImage, QIcon
import cv2
# Import our enhanced display widget for better video rendering
from ui.enhanced_simple_live_display import SimpleLiveDisplay
from utils.annotation_utils import convert_cv_to_pixmap
import os
import sys
import time
import numpy as np
class LiveTab(QWidget):
"""Live video processing and detection tab."""
video_dropped = Signal(str) # Emitted when video is dropped onto display
source_changed = Signal(object) # Emitted when video source changes
snapshot_requested = Signal() # Emitted when snapshot button is clicked
run_requested = Signal(bool) # Emitted when run/stop button is clicked
def __init__(self):
super().__init__()
self.current_source = 0 # Default to camera
self.initUI()
def initUI(self):
"""Initialize UI components"""
layout = QVBoxLayout(self)
# Video display - use simple label-based display
self.display = SimpleLiveDisplay()
layout.addWidget(self.display)
# Connect drag and drop signal from the display
self.display.video_dropped.connect(self.video_dropped)
# Control panel
controls = QHBoxLayout()
# Source selection
self.source_combo = QComboBox()
self.source_combo.addItem("📹 Camera 0", 0)
self.source_combo.addItem("📁 Video File", "file")
self.source_combo.setCurrentIndex(0)
self.source_combo.currentIndexChanged.connect(self.on_source_changed)
self.file_btn = QPushButton("📂 Browse")
self.file_btn.setMaximumWidth(100)
self.file_btn.clicked.connect(self.browse_files)
self.snapshot_btn = QPushButton("📸 Snapshot")
self.snapshot_btn.clicked.connect(self.snapshot_requested)
# Run/Stop button
self.run_btn = QPushButton("▶️ Run")
self.run_btn.setCheckable(True)
self.run_btn.clicked.connect(self.on_run_clicked)
self.run_btn.setStyleSheet("QPushButton:checked { background-color: #f44336; color: white; }")
# Performance metrics
self.fps_label = QLabel("FPS: -- | Inference: -- ms")
self.fps_label.setObjectName("fpsLabel")
self.fps_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
# Add controls to layout
src_layout = QHBoxLayout()
src_layout.addWidget(QLabel("Source:"))
src_layout.addWidget(self.source_combo)
src_layout.addWidget(self.file_btn)
controls.addLayout(src_layout)
controls.addWidget(self.run_btn)
controls.addWidget(self.snapshot_btn)
controls.addStretch(1)
controls.addWidget(self.fps_label)
layout.addLayout(controls)
# Status bar
status_bar = QHBoxLayout()
self.status_label = QLabel("Ready")
status_bar.addWidget(self.status_label)
layout.addLayout(status_bar)
@Slot()
def on_source_changed(self):
"""Handle source selection change"""
source_data = self.source_combo.currentData()
print(f"DEBUG: on_source_changed - current data: {source_data} (type: {type(source_data)})")
if source_data == "file":
# If "Video File" option is selected, open file dialog
self.browse_files()
return # browse_files will emit the signal
# For camera or specific file path
if isinstance(source_data, str) and os.path.isfile(source_data):
self.current_source = source_data
print(f"DEBUG: emitting source_changed with file path: {source_data}")
self.source_changed.emit(source_data)
elif source_data == 0:
self.current_source = 0
print(f"DEBUG: emitting source_changed with camera index 0")
self.source_changed.emit(0)
else:
print(f"WARNING: Unknown source_data: {source_data}")
@Slot()
def browse_files(self):
"""Open file dialog to select video file"""
file_path, _ = QFileDialog.getOpenFileName(
self, "Open Video File", "",
"Video Files (*.mp4 *.avi *.mov *.mkv *.webm);;All Files (*)"
)
if file_path:
print(f"DEBUG: Selected file: {file_path} (type: {type(file_path)})")
# Always add or select the file path in the combo box
existing_idx = self.source_combo.findData(file_path)
if existing_idx == -1:
self.source_combo.addItem(os.path.basename(file_path), file_path)
self.source_combo.setCurrentIndex(self.source_combo.count() - 1)
else:
self.source_combo.setCurrentIndex(existing_idx)
self.current_source = file_path
print(f"DEBUG: Setting current_source to: {self.current_source}")
print(f"DEBUG: emitting source_changed with {file_path}")
self.source_changed.emit(file_path)
else:
# If user cancels, revert to previous valid source
if isinstance(self.current_source, str) and os.path.isfile(self.current_source):
idx = self.source_combo.findData(self.current_source)
if idx != -1:
self.source_combo.setCurrentIndex(idx)
else:
self.source_combo.setCurrentIndex(0)
@Slot(bool)
def on_run_clicked(self, checked):
"""Handle run/stop button clicks"""
if checked:
self.run_btn.setText("⏹️ Stop")
print(f"DEBUG: on_run_clicked - current_source: {self.current_source} (type: {type(self.current_source)})")
if isinstance(self.current_source, str) and os.path.isfile(self.current_source):
print(f"DEBUG: Re-emitting source_changed with file: {self.current_source}")
self.source_changed.emit(self.current_source)
QTimer.singleShot(500, lambda: self.run_requested.emit(True))
elif self.current_source == 0:
print(f"DEBUG: Re-emitting source_changed with camera index 0")
self.source_changed.emit(0)
QTimer.singleShot(500, lambda: self.run_requested.emit(True))
else:
print("ERROR: No valid source selected")
self.run_btn.setChecked(False)
self.run_btn.setText("▶️ Run")
return
self.status_label.setText(f"Running... (Source: {self.current_source})")
else:
self.run_btn.setText("▶️ Run")
self.run_requested.emit(False)
self.status_label.setText("Stopped")
@Slot(object, object, dict)
def update_display(self, pixmap, detections, metrics):
"""Update display with processed frame (detections only)"""
if pixmap:
# Print debug info about the pixmap
print(f"DEBUG: Received pixmap: {pixmap.width()}x{pixmap.height()}, null: {pixmap.isNull()}")
# Ensure pixmap is valid
if not pixmap.isNull():
# --- COMMENTED OUT: Draw vehicle info for all detections (ID below bbox) ---
# for det in detections:
# if 'bbox' in det and 'id' in det:
# x1, y1, x2, y2 = det['bbox']
# vehicle_id = det['id']
# class_name = det.get('class_name', 'object')
# confidence = det.get('confidence', 0.0)
# color = (0, 255, 0)
# if class_name == 'traffic light':
# color = (0, 0, 255)
# label_text = f"{class_name}:{confidence:.2f}" # Removed vehicle_id from label
# label_y = y2 + 20
# cv2.putText(frame, label_text, (x1, label_y), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
# --- END COMMENTED BLOCK ---
self.display.update_frame(pixmap)
# Update metrics display
fps = metrics.get('FPS', '--')
detection_time = metrics.get('Detection (ms)', '--')
self.fps_label.setText(f"FPS: {fps} | Detection: {detection_time} ms")
# Update status with detection counts and traffic light status
detection_counts = {}
traffic_light_statuses = []
for det in detections:
class_name = det.get('class_name', 'unknown')
detection_counts[class_name] = detection_counts.get(class_name, 0) + 1
# Check for traffic light color
if class_name == 'traffic light' and 'traffic_light_color' in det:
color = det['traffic_light_color']
# Handle both dict and string for color
if isinstance(color, dict):
color_str = color.get('color', 'unknown')
else:
color_str = str(color)
traffic_light_statuses.append(f"Traffic Light: {color_str.upper()}")
# Show traffic light status if available
if traffic_light_statuses:
self.status_label.setText(" | ".join(traffic_light_statuses))
# Otherwise show detection counts
elif detection_counts:
sorted_counts = sorted(
detection_counts.items(),
key=lambda x: x[1],
reverse=True
)[:3]
status_text = " | ".join([
f"{cls}: {count}" for cls, count in sorted_counts
])
self.status_label.setText(status_text)
else:
self.status_label.setText("No detections")
else:
print("ERROR: Received null pixmap in update_display")
@Slot(np.ndarray)
def update_display_np(self, frame):
"""Update display with direct NumPy frame (optional)"""
print(f"🟢 Frame received in UI - LiveTab.update_display_np called")
print(f"🔵 Frame info: type={type(frame)}, shape={getattr(frame, 'shape', 'None')}")
if frame is None or not isinstance(frame, np.ndarray) or frame.size == 0:
print("⚠️ Received None or empty frame in update_display_np")
return
# Ensure BGR to RGB conversion for OpenCV frames
try:
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w, ch = rgb_frame.shape
bytes_per_line = ch * w
qimg = QImage(rgb_frame.data, w, h, bytes_per_line, QImage.Format_RGB888)
pixmap = QPixmap.fromImage(qimg)
# Scale pixmap to fit display
scaled_pixmap = pixmap.scaled(
self.display.width(), self.display.height(),
Qt.KeepAspectRatio, Qt.SmoothTransformation
)
print("📺 Sending scaled pixmap to display widget")
self.display.update_frame(scaled_pixmap)
except Exception as e:
print(f"❌ Error displaying frame: {e}")
import traceback
traceback.print_exc()
self.status_label.setText(f"Error displaying frame: {str(e)[:30]}...")
def reset_display(self):
"""Reset display to empty state"""
empty_pixmap = QPixmap(640, 480)
empty_pixmap.fill(Qt.black)
self.display.update_frame(empty_pixmap)
self.fps_label.setText("FPS: -- | Inference: -- ms")
self.status_label.setText("Ready")
@Slot(dict)
def update_stats(self, stats):
"""Update performance statistics display"""
# Extract values from stats dictionary
fps = stats.get('fps', 0.0)
detection_time = stats.get('detection_time', 0.0)
traffic_light_color = stats.get('traffic_light_color', 'unknown')
print(f"🟢 Stats Updated: FPS={fps:.2f}, Inference={detection_time:.2f}ms, Traffic Light={traffic_light_color}")
self.fps_label.setText(f"FPS: {fps:.1f}")
# Update status with traffic light information if available
if traffic_light_color != 'unknown':
# Create colorful text for traffic light
# Handle both dictionary and string formats
if isinstance(traffic_light_color, dict):
color_text = traffic_light_color.get("color", "unknown").upper()
else:
color_text = str(traffic_light_color).upper()
# Set text with traffic light information prominently displayed
self.status_label.setText(f"Inference: {detection_time:.1f} ms | 🚦 Traffic Light: {color_text}")
else:
self.status_label.setText(f"Inference: {detection_time:.1f} ms")
@Slot(np.ndarray, object, object, str, int)
def update_display_with_violations(self, frame, detections, violations, traffic_light_state, frame_idx):
"""
Update display with frame, detections, and violations overlay from controller logic
"""
# Draw overlay using the new logic (now in controller, not external)
violation_line_y = None
if violations and len(violations) > 0:
violation_line_y = violations[0]['details'].get('violation_line_y', None)
frame_with_overlay = self._draw_violation_overlay(frame, violations, violation_line_y)
pixmap = convert_cv_to_pixmap(frame_with_overlay)
self.display.update_frame(pixmap)
self.status_label.setText(f"Violations: {len(violations)} | Traffic Light: {traffic_light_state.upper()} | Frame: {frame_idx}")
def _draw_violation_overlay(self, frame, violations, violation_line_y=None, vehicle_tracks=None):
frame_copy = frame.copy()
violation_color = (0, 140, 255) # Orange
if violation_line_y is not None:
cv2.line(frame_copy, (0, violation_line_y), (frame.shape[1], violation_line_y), violation_color, 3)
cv2.putText(frame_copy, "VIOLATION LINE", (10, violation_line_y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, violation_color, 2)
for violation in violations:
bbox = violation['details']['bbox']
confidence = violation['details']['confidence']
vehicle_type = violation['details']['vehicle_type']
vehicle_id = violation.get('id', None)
x1, y1, x2, y2 = bbox
color = violation_color
label = f"VIOLATION: {vehicle_type.upper()}"
print(f"\033[93m[OVERLAY DRAW] Drawing violation overlay: ID={vehicle_id}, BBOX={bbox}, TYPE={vehicle_type}, CONF={confidence:.2f}\033[0m")
cv2.rectangle(frame_copy, (x1, y1), (x2, y2), color, 3)
cv2.putText(frame_copy, label, (x1, y1 - 40), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
cv2.putText(frame_copy, f"Confidence: {confidence:.2f}", (x1, y1 - 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
if vehicle_id is not None:
cv2.putText(frame_copy, f"ID: {vehicle_id}", (x1, y2 + 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
if vehicle_tracks is not None:
for track_id, track in vehicle_tracks.items():
for pos in track['positions']:
cv2.circle(frame_copy, pos, 3, (255, 0, 255), -1)
return frame_copy
@Slot(np.ndarray, list, list)
def update_display_np_with_violations(self, frame, detections, violators):
"""
Display annotated frame and highlight violators in orange, print violations to console.
Args:
frame (np.ndarray): Already-annotated frame from controller.
detections (list): List of all vehicle detections (with id, bbox).
violators (list): List of violator dicts (with id, bbox, etc.).
"""
print(f"🟢 Frame received in UI - update_display_np_with_violations called")
print(f"🔵 Frame info: type={type(frame)}, shape={getattr(frame, 'shape', 'None')}")
if frame is None or not isinstance(frame, np.ndarray) or frame.size == 0:
print("⚠️ Received None or empty frame in update_display_np_with_violations")
return
frame_disp = frame.copy()
# Draw orange boxes for violators
for v in violators:
bbox = v.get('bbox')
vid = v.get('id')
if bbox is not None and len(bbox) == 4:
x1, y1, x2, y2 = map(int, bbox)
cv2.rectangle(frame_disp, (x1, y1), (x2, y2), (0,140,255), 4)
cv2.putText(frame_disp, f"VIOLATION ID:{vid}", (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,140,255), 2)
print(f"[VIOLATION] Vehicle {vid} crossed at bbox {bbox}")
pixmap = convert_cv_to_pixmap(frame_disp)
print("📺 Sending frame to display widget")
self.display.update_frame(pixmap)
print("✅ Frame passed to display widget successfully")
self.status_label.setText(f"Frame displayed: {frame.shape[1]}x{frame.shape[0]}, Violations: {len(violators)}")

View File

@@ -0,0 +1,25 @@
from PySide6.QtWidgets import QWidget, QHBoxLayout, QLabel
class GlobalStatusPanel(QWidget):
def __init__(self):
super().__init__()
layout = QHBoxLayout()
self.model_label = QLabel("Model: -")
self.device_label = QLabel("Device: -")
self.yolo_label = QLabel("YOLO Version: -")
self.resolution_label = QLabel("Resolution: -")
self.fps_labels = [QLabel(f"CAM {i+1} FPS: -") for i in range(4)]
layout.addWidget(self.model_label)
layout.addWidget(self.device_label)
layout.addWidget(self.yolo_label)
layout.addWidget(self.resolution_label)
for lbl in self.fps_labels:
layout.addWidget(lbl)
self.setLayout(layout)
def update_status(self, model, device, yolo, resolution, fps_list):
self.model_label.setText(f"Model: {model}")
self.device_label.setText(f"Device: {device}")
self.yolo_label.setText(f"YOLO Version: {yolo}")
self.resolution_label.setText(f"Resolution: {resolution}")
for i, fps in enumerate(fps_list):
self.fps_labels[i].setText(f"CAM {i+1} FPS: {fps}")

View File

@@ -0,0 +1,168 @@
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QIcon, QImage, QPixmap
from PySide6.QtWidgets import QWidget, QGridLayout, QVBoxLayout, QLabel, QPushButton, QHBoxLayout, QFrame, QComboBox, QCheckBox
import cv2
import numpy as np
class CameraFeedWidget(QFrame):
settings_clicked = Signal(int)
detection_toggled = Signal(int, bool)
def __init__(self, cam_number):
super().__init__()
self.cam_number = cam_number
self.setFrameShape(QFrame.Box)
self.setLineWidth(3)
self.setStyleSheet("QFrame { border: 3px solid gray; border-radius: 8px; }")
layout = QVBoxLayout()
top_bar = QHBoxLayout()
self.overlay_label = QLabel(f"<b>CAM {cam_number}</b>")
self.gear_btn = QPushButton()
self.gear_btn.setIcon(QIcon.fromTheme("settings"))
self.gear_btn.setFixedSize(24,24)
self.gear_btn.clicked.connect(lambda: self.settings_clicked.emit(self.cam_number))
top_bar.addWidget(self.overlay_label)
top_bar.addStretch()
top_bar.addWidget(self.gear_btn)
layout.addLayout(top_bar)
self.video_label = QLabel("No Feed")
self.video_label.setMinimumHeight(160)
self.fps_label = QLabel("FPS: 0")
self.count_label = QLabel("Cars: 0 | Trucks: 0 | Ped: 0 | TLights: 0 | Moto: 0")
self.detection_toggle = QCheckBox("Detection ON")
self.detection_toggle.setChecked(True)
self.detection_toggle.toggled.connect(lambda checked: self.detection_toggled.emit(self.cam_number, checked))
self.start_stop_btn = QPushButton("Start")
layout.addWidget(self.video_label)
layout.addWidget(self.fps_label)
layout.addWidget(self.count_label)
layout.addWidget(self.detection_toggle)
layout.addWidget(self.start_stop_btn)
self.setLayout(layout)
def set_active(self, active):
color = "#00FF00" if active else "gray"
self.setStyleSheet(f"QFrame {{ border: 3px solid {color}; border-radius: 8px; }}")
class LiveMultiCamTab(QWidget):
source_changed = Signal(int, object) # cam_number, source
run_requested = Signal(int, bool) # cam_number, start/stop
detection_toggled = Signal(int, bool) # cam_number, enabled
settings_clicked = Signal(int)
global_detection_toggled = Signal(bool)
device_changed = Signal(str)
video_dropped = Signal(int, object) # cam_number, dropped source
snapshot_requested = Signal(int) # cam_number
def __init__(self):
super().__init__()
# Info bar at the top (only for Live Detection tab)
info_bar = QHBoxLayout()
self.model_label = QLabel("Model: -")
self.device_label = QLabel("Device: -")
self.yolo_label = QLabel("YOLO Version: -")
self.resolution_label = QLabel("Resolution: -")
self.cam1_fps = QLabel("CAM 1 FPS: -")
self.cam2_fps = QLabel("CAM 2 FPS: -")
self.cam3_fps = QLabel("CAM 3 FPS: -")
self.cam4_fps = QLabel("CAM 4 FPS: -")
info_bar.addWidget(self.model_label)
info_bar.addWidget(self.device_label)
info_bar.addWidget(self.yolo_label)
info_bar.addWidget(self.resolution_label)
info_bar.addWidget(self.cam1_fps)
info_bar.addWidget(self.cam2_fps)
info_bar.addWidget(self.cam3_fps)
info_bar.addWidget(self.cam4_fps)
info_bar.addStretch()
grid = QGridLayout()
self.cameras = []
for i in range(4):
cam_widget = CameraFeedWidget(i+1)
cam_widget.start_stop_btn.clicked.connect(lambda checked, n=i+1: self._handle_start_stop(n))
cam_widget.settings_clicked.connect(self.settings_clicked.emit)
cam_widget.detection_toggled.connect(self.detection_toggled.emit)
# Add snapshot button for each camera
snapshot_btn = QPushButton("Snapshot")
snapshot_btn.clicked.connect(lambda checked=False, n=i+1: self.snapshot_requested.emit(n))
cam_widget.layout().addWidget(snapshot_btn)
self.cameras.append(cam_widget)
grid.addWidget(cam_widget, i//2, i%2)
controls = QHBoxLayout()
self.start_all_btn = QPushButton("Start All")
self.stop_all_btn = QPushButton("Stop All")
self.global_detection_toggle = QCheckBox("Detection ON (All)")
self.global_detection_toggle.setChecked(True)
self.device_selector = QComboBox()
self.device_selector.addItems(["CPU", "GPU", "NPU"])
self.start_all_btn.clicked.connect(lambda: self._handle_all(True))
self.stop_all_btn.clicked.connect(lambda: self._handle_all(False))
self.global_detection_toggle.toggled.connect(self.global_detection_toggled.emit)
self.device_selector.currentTextChanged.connect(self.device_changed.emit)
controls.addWidget(self.start_all_btn)
controls.addWidget(self.stop_all_btn)
controls.addWidget(self.global_detection_toggle)
controls.addWidget(QLabel("Device:"))
controls.addWidget(self.device_selector)
main_layout = QVBoxLayout()
main_layout.addLayout(info_bar)
main_layout.addLayout(grid)
main_layout.addLayout(controls)
self.setLayout(main_layout)
def _handle_start_stop(self, cam_number):
btn = self.cameras[cam_number-1].start_stop_btn
start = btn.text() == "Start"
self.run_requested.emit(cam_number, start)
btn.setText("Stop" if start else "Start")
def _handle_all(self, start):
for i, cam in enumerate(self.cameras):
self.run_requested.emit(i+1, start)
cam.start_stop_btn.setText("Stop" if start else "Start")
def update_display(self, cam_number, pixmap):
# If pixmap is None, show a user-friendly message and disable controls
if pixmap is None:
self.cameras[cam_number-1].video_label.setText("No feed. Click 'Start' to connect a camera or select a video.")
self.cameras[cam_number-1].video_label.setStyleSheet("color: #F44336; font-size: 15px; background: transparent;")
self._set_controls_enabled(cam_number-1, False)
else:
self.cameras[cam_number-1].video_label.setPixmap(pixmap)
self.cameras[cam_number-1].video_label.setStyleSheet("background: transparent;")
self._set_controls_enabled(cam_number-1, True)
def _set_controls_enabled(self, cam_idx, enabled):
for btn in [self.cam_widgets[cam_idx]['start_btn'], self.cam_widgets[cam_idx]['snapshot_btn']]:
btn.setEnabled(enabled)
def update_display_np(self, np_frame):
"""Display a NumPy frame in CAM 1 (single source live mode)."""
import cv2
import numpy as np
if np_frame is None or not isinstance(np_frame, np.ndarray) or np_frame.size == 0:
print(f"[LiveMultiCamTab] ⚠️ Received None or empty frame for CAM 1")
return
try:
rgb_frame = cv2.cvtColor(np_frame, cv2.COLOR_BGR2RGB)
h, w, ch = rgb_frame.shape
from PySide6.QtGui import QImage, QPixmap
from PySide6.QtCore import Qt
bytes_per_line = ch * w
qimg = QImage(rgb_frame.data, w, h, bytes_per_line, QImage.Format_RGB888)
pixmap = QPixmap.fromImage(qimg)
scaled_pixmap = pixmap.scaled(
self.cameras[0].video_label.width(),
self.cameras[0].video_label.height(),
Qt.KeepAspectRatio, Qt.SmoothTransformation
)
self.cameras[0].video_label.setPixmap(scaled_pixmap)
self.cameras[0].video_label.update()
print(f"[LiveMultiCamTab] 🟢 Frame displayed for CAM 1")
except Exception as e:
print(f"[LiveMultiCamTab] ❌ Error displaying frame for CAM 1: {e}")
import traceback
traceback.print_exc()
def update_fps(self, cam_number, fps):
self.cameras[cam_number-1].fps_label.setText(f"FPS: {fps}")
def update_counts(self, cam_number, cars, trucks, peds, tlights, motorcycles):
self.cameras[cam_number-1].count_label.setText(
f"Cars: {cars} | Trucks: {trucks} | Ped: {peds} | TLights: {tlights} | Moto: {motorcycles}")
def update_stats(self, cam_number, stats):
# Placeholder: expects stats dict with keys: cars, trucks, peds, tlights, motorcycles, fps
self.update_counts(cam_number, stats.get('cars', 0), stats.get('trucks', 0), stats.get('peds', 0), stats.get('tlights', 0), stats.get('motorcycles', 0))
self.update_fps(cam_number, stats.get('fps', 0))
def set_detection_active(self, cam_number, active):
self.cameras[cam_number-1].set_active(active)

View File

@@ -0,0 +1,283 @@
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QFileDialog, QComboBox, QGroupBox, QToolButton
)
from PySide6.QtCore import Qt, Signal, QSize, Slot, QTimer
from PySide6.QtGui import QPixmap, QImage, QIcon
# Import our enhanced display widget for better video rendering
from ui.enhanced_simple_live_display import SimpleLiveDisplay
import os
import sys
import time
import numpy as np
class LiveTab(QWidget):
"""Live video processing and detection tab."""
video_dropped = Signal(str) # Emitted when video is dropped onto display
source_changed = Signal(object) # Emitted when video source changes
snapshot_requested = Signal() # Emitted when snapshot button is clicked
run_requested = Signal(bool) # Emitted when run/stop button is clicked
def __init__(self):
super().__init__()
self.current_source = 0 # Default to camera
self.initUI()
def initUI(self):
"""Initialize UI components"""
layout = QVBoxLayout(self)
# Video display - use simple label-based display
self.display = SimpleLiveDisplay()
layout.addWidget(self.display)
# Connect drag and drop signal from the display
self.display.video_dropped.connect(self.video_dropped)
# Control panel
controls = QHBoxLayout()
# Source selection
self.source_combo = QComboBox()
self.source_combo.addItem("📹 Camera 0", 0)
self.source_combo.addItem("📁 Video File", "file")
self.source_combo.setCurrentIndex(0)
self.source_combo.currentIndexChanged.connect(self.on_source_changed)
self.file_btn = QPushButton("📂 Browse")
self.file_btn.setMaximumWidth(100)
self.file_btn.clicked.connect(self.browse_files)
self.snapshot_btn = QPushButton("📸 Snapshot")
self.snapshot_btn.clicked.connect(self.snapshot_requested)
# Run/Stop button
self.run_btn = QPushButton("▶️ Run")
self.run_btn.setCheckable(True)
self.run_btn.clicked.connect(self.on_run_clicked)
self.run_btn.setStyleSheet("QPushButton:checked { background-color: #f44336; color: white; }")
# Performance metrics
self.fps_label = QLabel("FPS: -- | Inference: -- ms")
self.fps_label.setObjectName("fpsLabel")
self.fps_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
# Add controls to layout
src_layout = QHBoxLayout()
src_layout.addWidget(QLabel("Source:"))
src_layout.addWidget(self.source_combo)
src_layout.addWidget(self.file_btn)
controls.addLayout(src_layout)
controls.addWidget(self.run_btn)
controls.addWidget(self.snapshot_btn)
controls.addStretch(1)
controls.addWidget(self.fps_label)
layout.addLayout(controls)
# Status bar
status_bar = QHBoxLayout()
self.status_label = QLabel("Ready")
status_bar.addWidget(self.status_label)
layout.addLayout(status_bar)
@Slot()
def on_source_changed(self):
"""Handle source selection change"""
source_data = self.source_combo.currentData()
print(f"DEBUG: on_source_changed - current data: {source_data} (type: {type(source_data)})")
if source_data == "file":
# If "Video File" option is selected, open file dialog
self.browse_files()
return # browse_files will emit the signal
# For camera or specific file path
self.current_source = source_data
print(f"DEBUG: emitting source_changed with {source_data} (type: {type(source_data)})")
self.source_changed.emit(source_data)
@Slot()
def browse_files(self):
"""Open file dialog to select video file"""
file_path, _ = QFileDialog.getOpenFileName(
self, "Open Video File", "",
"Video Files (*.mp4 *.avi *.mov *.mkv *.webm);;All Files (*)"
)
if file_path:
print(f"DEBUG: Selected file: {file_path} (type: {type(file_path)})")
# First set dropdown to "Video File" option
file_idx = self.source_combo.findData("file")
if file_idx >= 0:
self.source_combo.setCurrentIndex(file_idx)
# Then add the specific file
existing_idx = self.source_combo.findData(file_path)
if existing_idx == -1:
# Add new item
self.source_combo.addItem(os.path.basename(file_path), file_path)
self.source_combo.setCurrentIndex(self.source_combo.count() - 1)
else:
# Select existing item
self.source_combo.setCurrentIndex(existing_idx)
# Update current source
self.current_source = file_path
print(f"DEBUG: Setting current_source to: {self.current_source}")
print(f"DEBUG: emitting source_changed with {file_path}")
self.source_changed.emit(file_path)
@Slot(bool)
def on_run_clicked(self, checked):
"""Handle run/stop button clicks"""
if checked:
# If run is clicked, ensure we're using the current source
self.run_btn.setText("⏹️ Stop")
# Print detailed debug info
print(f"DEBUG: on_run_clicked - current_source: {self.current_source} (type: {type(self.current_source)})")
# First ensure the correct source is set before running
if self.current_source is not None:
# Re-emit the source to make sure it's properly set
print(f"DEBUG: Re-emitting source_changed with: {self.current_source}")
self.source_changed.emit(self.current_source)
# Use a timer to give the source time to be set
QTimer.singleShot(500, lambda: self.run_requested.emit(True))
else:
print("ERROR: No source selected")
self.run_btn.setChecked(False)
self.run_btn.setText("▶️ Run")
return
self.status_label.setText(f"Running... (Source: {self.current_source})")
else:
self.run_btn.setText("▶️ Run")
self.run_requested.emit(False)
self.status_label.setText("Stopped")
@Slot(object, object, dict)
def update_display(self, pixmap, detections, metrics):
"""Update display with processed frame (detections only)"""
if pixmap:
# Print debug info about the pixmap
print(f"DEBUG: Received pixmap: {pixmap.width()}x{pixmap.height()}, null: {pixmap.isNull()}")
# Ensure pixmap is valid
if not pixmap.isNull():
self.display.update_frame(pixmap)
# Update metrics display
fps = metrics.get('FPS', '--')
detection_time = metrics.get('Detection (ms)', '--')
self.fps_label.setText(f"FPS: {fps} | Detection: {detection_time} ms")
# Update status with detection counts
detection_counts = {}
for det in detections:
class_name = det.get('class_name', 'unknown')
detection_counts[class_name] = detection_counts.get(class_name, 0) + 1
# Show top 3 detected classes
if detection_counts:
sorted_counts = sorted(
detection_counts.items(),
key=lambda x: x[1],
reverse=True
)[:3]
status_text = " | ".join([
f"{cls}: {count}" for cls, count in sorted_counts
])
self.status_label.setText(status_text)
else:
self.status_label.setText("No detections")
else:
print("ERROR: Received null pixmap in update_display")
@Slot(np.ndarray)
def update_display_np(self, frame):
"""Update display with direct NumPy frame (optional)"""
print(f"<EFBFBD> Frame received in UI - LiveTab.update_display_np called")
print(f"🔵 Frame info: type={type(frame)}, shape={getattr(frame, 'shape', 'None')}")
if frame is None:
print("⚠️ Received None frame in update_display_np")
return
if not isinstance(frame, np.ndarray):
print(f"⚠️ Received non-numpy frame type: {type(frame)}")
return
if frame.size == 0 or frame.shape[0] == 0 or frame.shape[1] == 0:
print(f"⚠️ Received empty frame with shape: {frame.shape}")
return
try:
# Make sure we have a fresh copy of the data
frame_copy = frame.copy()
# Display the frame through our display widget
print("📺 Sending frame to display widget")
self.display.display_frame(frame_copy)
print("✅ Frame passed to display widget successfully")
except Exception as e:
print(f"❌ Error displaying frame: {e}")
import traceback
traceback.print_exc()
def reset_display(self):
"""Reset display to empty state"""
empty_pixmap = QPixmap(640, 480)
empty_pixmap.fill(Qt.black)
self.display.update_frame(empty_pixmap)
self.fps_label.setText("FPS: -- | Inference: -- ms")
self.status_label.setText("Ready")
@Slot(dict)
def update_stats(self, stats):
"""Update performance statistics display"""
# Extract values from stats dictionary
fps = stats.get('fps', 0.0)
detection_time = stats.get('detection_time', 0.0)
traffic_light_info = stats.get('traffic_light_color', 'unknown')
# Handle both string and dictionary formats for traffic light color
if isinstance(traffic_light_info, dict):
traffic_light_color = traffic_light_info.get('color', 'unknown')
confidence = traffic_light_info.get('confidence', 0.0)
confidence_text = f" (Conf: {confidence:.2f})"
else:
traffic_light_color = traffic_light_info
confidence_text = ""
print(f"🟢 Stats Updated: FPS={fps:.2f}, Inference={detection_time:.2f}ms, Traffic Light={traffic_light_color}{confidence_text}")
self.fps_label.setText(f"FPS: {fps:.1f}")
# Update status with traffic light information if available
if traffic_light_color != 'unknown':
# Create colorful text for traffic light
color_text = str(traffic_light_color).upper()
# Set color-coded style based on traffic light color
color_style = ""
if color_text == "RED":
color_style = "color: red; font-weight: bold;"
elif color_text == "YELLOW":
color_style = "color: #FFD700; font-weight: bold;" # Golden yellow for better visibility
elif color_text == "GREEN":
color_style = "color: green; font-weight: bold;"
# Set text with traffic light information prominently displayed
self.status_label.setText(f"Inference: {detection_time:.1f} ms | 🚦 Traffic Light: <span style='{color_style}'>{color_text}</span>{confidence_text}")
# Print the status to console too for debugging
if isinstance(traffic_light_info, dict) and 'confidence' in traffic_light_info:
print(f"🚦 UI Updated: Traffic Light = {color_text} (Confidence: {confidence:.2f})")
else:
print(f"🚦 UI Updated: Traffic Light = {color_text}")
else:
self.status_label.setText(f"Inference: {detection_time:.1f} ms")

View File

@@ -0,0 +1,750 @@
from PySide6.QtWidgets import (
QMainWindow, QTabWidget, QDockWidget, QMessageBox,
QApplication, QFileDialog, QSplashScreen, QVBoxLayout, QWidget
)
from PySide6.QtCore import Qt, QTimer, QSettings, QSize, Slot
from PySide6.QtGui import QIcon, QPixmap, QAction
import os
import sys
import json
import time
import traceback
from pathlib import Path
# Custom exception handler for Qt
def qt_message_handler(mode, context, message):
print(f"Qt Message: {message} (Mode: {mode})")
# Install custom handler for Qt messages
if hasattr(Qt, 'qInstallMessageHandler'):
Qt.qInstallMessageHandler(qt_message_handler)
# Import UI components
from ui.analytics_tab import AnalyticsTab
from ui.violations_tab import ViolationsTab
from ui.export_tab import ExportTab
from ui.config_panel import ConfigPanel
from ui.live_multi_cam_tab import LiveMultiCamTab
from ui.video_detection_tab import VideoDetectionTab
from ui.global_status_panel import GlobalStatusPanel
# Import controllers
from controllers.video_controller_new import VideoController
from controllers.analytics_controller import AnalyticsController
from controllers.performance_overlay import PerformanceOverlay
from controllers.model_manager import ModelManager
# Import utilities
from utils.helpers import load_configuration, save_configuration, save_snapshot
class MainWindow(QMainWindow):
"""Main application window."""
def __init__(self):
super().__init__()
# Initialize settings and configuration
self.settings = QSettings("OpenVINO", "TrafficMonitoring")
self.config_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.json")
self.config = load_configuration(self.config_file)
# Set up UI
self.setupUI()
# Initialize controllers
self.setupControllers()
# Connect signals and slots
self.connectSignals()
# Restore settings
self.restoreSettings()
# Apply theme
self.applyTheme(True) # Start with dark theme
# Show ready message
self.statusBar().showMessage("Ready")
def setupUI(self):
"""Set up the user interface"""
# Window properties
self.setWindowTitle("Traffic Monitoring System (OpenVINO PySide6)")
self.setMinimumSize(1200, 800)
self.resize(1400, 900)
# Set up central widget with tabs
self.tabs = QTabWidget()
# Create tabs
self.live_tab = LiveMultiCamTab()
self.video_detection_tab = VideoDetectionTab()
self.analytics_tab = AnalyticsTab()
self.violations_tab = ViolationsTab()
self.export_tab = ExportTab()
from ui.performance_graphs import PerformanceGraphsWidget
self.performance_tab = PerformanceGraphsWidget()
# Add tabs to tab widget
self.tabs.addTab(self.live_tab, "Live Detection")
self.tabs.addTab(self.video_detection_tab, "Video Detection")
self.tabs.addTab(self.performance_tab, "🔥 Performance & Latency")
self.tabs.addTab(self.analytics_tab, "Analytics")
self.tabs.addTab(self.violations_tab, "Violations")
self.tabs.addTab(self.export_tab, "Export & Config")
# Create config panel in dock widget
self.config_panel = ConfigPanel()
dock = QDockWidget("Settings", self)
dock.setObjectName("SettingsDock") # Set object name to avoid warning
dock.setWidget(self.config_panel)
dock.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetClosable)
dock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.addDockWidget(Qt.RightDockWidgetArea, dock)
# Create status bar
self.statusBar().showMessage("Initializing...")
main_layout = QVBoxLayout()
main_layout.addWidget(self.tabs)
central = QWidget()
central.setLayout(main_layout)
self.setCentralWidget(central)
# Create menu bar
self.setupMenus()
# Create performance overlay
self.performance_overlay = PerformanceOverlay()
def setupControllers(self):
"""Set up controllers and models"""
try:
# Initialize model manager
self.model_manager = ModelManager(self.config_file)
# Create video controller for live tab
self.video_controller = VideoController(self.model_manager)
# Create video controller for video detection tab
self.video_file_controller = VideoController(self.model_manager)
# Create analytics controller
self.analytics_controller = AnalyticsController()
# Setup update timer for performance overlay
self.perf_timer = QTimer()
self.perf_timer.timeout.connect(self.performance_overlay.update_stats)
self.perf_timer.start(1000) # Update every second
# Connect video_file_controller outputs to video_detection_tab
self.video_file_controller.frame_ready.connect(self.video_detection_tab.update_display, Qt.QueuedConnection)
self.video_file_controller.stats_ready.connect(self.video_detection_tab.update_stats, Qt.QueuedConnection)
self.video_file_controller.progress_ready.connect(lambda value, max_value, timestamp: self.video_detection_tab.update_progress(value, max_value, timestamp), Qt.QueuedConnection)
# Connect auto model/device selection signal
self.video_detection_tab.auto_select_model_device.connect(self.video_file_controller.auto_select_model_device, Qt.QueuedConnection)
except Exception as e:
QMessageBox.critical(
self,
"Initialization Error",
f"Error initializing controllers: {str(e)}"
)
print(f"Error details: {e}")
def connectSignals(self):
"""Connect signals and slots between components"""
print("🔌 Connecting video controller signals...")
try:
self.video_controller.frame_ready.connect(self.live_tab.update_display, Qt.QueuedConnection)
print("✅ Connected frame_ready signal")
try:
self.video_controller.frame_np_ready.connect(self.live_tab.update_display_np, Qt.QueuedConnection)
print("✅ Connected frame_np_ready signal")
print("🔌 frame_np_ready connection should be established")
except Exception as e:
print(f"❌ Error connecting frame_np_ready signal: {e}")
import traceback
traceback.print_exc()
self.video_controller.stats_ready.connect(self.live_tab.update_stats, Qt.QueuedConnection)
self.video_controller.stats_ready.connect(self.update_traffic_light_status, Qt.QueuedConnection)
print("✅ Connected stats_ready signals")
# Only connect analytics_controller if it exists
if hasattr(self, 'analytics_controller'):
self.video_controller.raw_frame_ready.connect(self.analytics_controller.process_frame_data)
print("✅ Connected raw_frame_ready signal")
else:
print("❌ analytics_controller not found, skipping analytics signal connection")
self.video_controller.stats_ready.connect(self.update_traffic_light_status, Qt.QueuedConnection)
print("✅ Connected stats_ready signal to update_traffic_light_status")
# Connect violation detection signal
try:
self.video_controller.violation_detected.connect(self.handle_violation_detected, Qt.QueuedConnection)
print("✅ Connected violation_detected signal")
except Exception as e:
print(f"⚠️ Could not connect violation signal: {e}")
except Exception as e:
print(f"❌ Error connecting signals: {e}")
import traceback
traceback.print_exc()
# Live tab connections
self.live_tab.source_changed.connect(self.video_controller.set_source)
self.live_tab.video_dropped.connect(self.video_controller.set_source)
self.live_tab.snapshot_requested.connect(self.take_snapshot)
self.live_tab.run_requested.connect(self.toggle_video_processing)
# Config panel connections
self.config_panel.config_changed.connect(self.apply_config)
self.config_panel.theme_toggled.connect(self.applyTheme)
# Connect device switch signal for robust model switching
self.config_panel.device_switch_requested.connect(self.handle_device_switch)
# Analytics controller connections
self.analytics_controller.analytics_updated.connect(self.analytics_tab.update_analytics)
self.analytics_controller.analytics_updated.connect(self.export_tab.update_export_preview)
# Tab-specific connections
self.violations_tab.clear_btn.clicked.connect(self.analytics_controller.clear_statistics)
self.export_tab.reset_btn.clicked.connect(self.config_panel.reset_config)
self.export_tab.save_config_btn.clicked.connect(self.save_config)
self.export_tab.reload_config_btn.clicked.connect(self.load_config)
self.export_tab.export_btn.clicked.connect(self.export_data)
# Video Detection tab connections
self.video_detection_tab.file_selected.connect(self._handle_video_file_selected)
self.video_detection_tab.play_clicked.connect(self._handle_video_play)
self.video_detection_tab.pause_clicked.connect(self._handle_video_pause)
self.video_detection_tab.stop_clicked.connect(self._handle_video_stop)
self.video_detection_tab.detection_toggled.connect(self._handle_video_detection_toggle)
self.video_detection_tab.screenshot_clicked.connect(self._handle_video_screenshot)
self.video_detection_tab.seek_changed.connect(self._handle_video_seek)
# Connect OpenVINO device info signal to config panel from BOTH controllers
self.video_controller.device_info_ready.connect(self.config_panel.update_devices_info, Qt.QueuedConnection)
self.video_file_controller.device_info_ready.connect(self.config_panel.update_devices_info, Qt.QueuedConnection)
# After connecting video_file_controller and video_detection_tab, trigger auto model/device update
QTimer.singleShot(0, self.video_file_controller.auto_select_model_device.emit)
self.video_controller.performance_stats_ready.connect(self.update_performance_graphs)
def setupMenus(self):
"""Set up application menus"""
# File menu
file_menu = self.menuBar().addMenu("&File")
open_action = QAction("&Open Video...", self)
open_action.setShortcut("Ctrl+O")
open_action.triggered.connect(self.open_video_file)
file_menu.addAction(open_action)
file_menu.addSeparator()
snapshot_action = QAction("Take &Snapshot", self)
snapshot_action.setShortcut("Ctrl+S")
snapshot_action.triggered.connect(self.take_snapshot)
file_menu.addAction(snapshot_action)
file_menu.addSeparator()
exit_action = QAction("E&xit", self)
exit_action.setShortcut("Alt+F4")
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# View menu
view_menu = self.menuBar().addMenu("&View")
toggle_config_action = QAction("Show/Hide &Settings Panel", self)
toggle_config_action.setShortcut("F4")
toggle_config_action.triggered.connect(self.toggle_config_panel)
view_menu.addAction(toggle_config_action)
toggle_perf_action = QAction("Show/Hide &Performance Overlay", self)
toggle_perf_action.setShortcut("F5")
toggle_perf_action.triggered.connect(self.toggle_performance_overlay)
view_menu.addAction(toggle_perf_action)
# Help menu
help_menu = self.menuBar().addMenu("&Help")
about_action = QAction("&About", self)
about_action.triggered.connect(self.show_about_dialog)
help_menu.addAction(about_action)
@Slot(dict)
def apply_config(self, config):
"""
Apply configuration changes.
Args:
config: Configuration dictionary
"""
# Update configuration
if not config:
return
# Update config
for section in config:
if section in self.config:
self.config[section].update(config[section])
else:
self.config[section] = config[section]
# Update model manager
if self.model_manager:
self.model_manager.update_config(self.config)
# Save config to file
save_configuration(self.config, self.config_file)
# Update export tab
self.export_tab.update_config_display(self.config)
# Update status
self.statusBar().showMessage("Configuration applied", 2000)
@Slot()
def load_config(self):
"""Load configuration from file"""
# Ask for confirmation if needed
if self.video_controller and self.video_controller._running:
reply = QMessageBox.question(
self,
"Reload Configuration",
"Reloading configuration will stop current processing. Continue?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.No:
return
# Stop processing
self.video_controller.stop()
# Load config
self.config = load_configuration(self.config_file)
# Update UI
self.config_panel.set_config(self.config)
self.export_tab.update_config_display(self.config)
# Update model manager
if self.model_manager:
self.model_manager.update_config(self.config)
# Update status
self.statusBar().showMessage("Configuration loaded", 2000)
@Slot()
def save_config(self):
"""Save configuration to file"""
# Get config from UI
ui_config = self.export_tab.get_config_from_ui()
# Update config
for section in ui_config:
if section in self.config:
self.config[section].update(ui_config[section])
else:
self.config[section] = ui_config[section]
# Save to file
if save_configuration(self.config, self.config_file):
self.statusBar().showMessage("Configuration saved", 2000)
else:
self.statusBar().showMessage("Error saving configuration", 2000)
# Update model manager
if self.model_manager:
self.model_manager.update_config(self.config)
@Slot()
def open_video_file(self):
"""Open video file dialog"""
file_path, _ = QFileDialog.getOpenFileName(
self,
"Open Video File",
"",
"Video Files (*.mp4 *.avi *.mov *.mkv *.webm);;All Files (*)"
)
if file_path:
# Update live tab
self.live_tab.source_changed.emit(file_path)
# Update status
self.statusBar().showMessage(f"Loaded video: {os.path.basename(file_path)}")
@Slot()
def take_snapshot(self):
"""Take snapshot of current frame"""
if self.video_controller:
# Get current frame
frame = self.video_controller.capture_snapshot()
if frame is not None:
# Save frame to file
save_dir = self.settings.value("snapshot_dir", ".")
file_path = os.path.join(save_dir, "snapshot_" +
str(int(time.time())) + ".jpg")
saved_path = save_snapshot(frame, file_path)
if saved_path:
self.statusBar().showMessage(f"Snapshot saved: {saved_path}", 3000)
else:
self.statusBar().showMessage("Error saving snapshot", 3000)
else:
self.statusBar().showMessage("No frame to capture", 3000)
@Slot()
def toggle_config_panel(self):
"""Toggle configuration panel visibility"""
dock_widgets = self.findChildren(QDockWidget)
for dock in dock_widgets:
dock.setVisible(not dock.isVisible())
@Slot()
def toggle_performance_overlay(self):
"""Toggle performance overlay visibility"""
if self.performance_overlay.isVisible():
self.performance_overlay.hide()
else:
# Position in the corner
self.performance_overlay.move(self.pos().x() + 10, self.pos().y() + 30)
self.performance_overlay.show()
@Slot(bool)
def applyTheme(self, dark_theme):
"""
Apply light or dark theme.
Args:
dark_theme: True for dark theme, False for light theme
"""
if dark_theme:
# Load dark theme stylesheet
theme_file = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
"resources", "themes", "dark.qss"
)
else:
# Load light theme stylesheet
theme_file = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
"resources", "themes", "light.qss"
)
# Apply theme if file exists
if os.path.exists(theme_file):
with open(theme_file, "r") as f:
self.setStyleSheet(f.read())
else:
# Fallback to built-in style
self.setStyleSheet("")
@Slot()
def export_data(self):
"""Export data to file"""
export_format = self.export_tab.export_format_combo.currentText()
export_data = self.export_tab.export_data_combo.currentText()
# Get file type filter based on format
if export_format == "CSV":
file_filter = "CSV Files (*.csv)"
default_ext = ".csv"
elif export_format == "JSON":
file_filter = "JSON Files (*.json)"
default_ext = ".json"
elif export_format == "Excel":
file_filter = "Excel Files (*.xlsx)"
default_ext = ".xlsx"
elif export_format == "PDF Report":
file_filter = "PDF Files (*.pdf)"
default_ext = ".pdf"
else:
file_filter = "All Files (*)"
default_ext = ".txt"
# Get save path
file_path, _ = QFileDialog.getSaveFileName(
self,
"Export Data",
f"traffic_data{default_ext}",
file_filter
)
if not file_path:
return
try:
# Get analytics data
analytics = self.analytics_controller.get_analytics()
# Export based on format
if export_format == "CSV":
from utils.helpers import create_export_csv
result = create_export_csv(analytics['detection_counts'], file_path)
elif export_format == "JSON":
from utils.helpers import create_export_json
result = create_export_json(analytics, file_path)
elif export_format == "Excel":
# Requires openpyxl
try:
import pandas as pd
df = pd.DataFrame({
'Class': list(analytics['detection_counts'].keys()),
'Count': list(analytics['detection_counts'].values())
})
df.to_excel(file_path, index=False)
result = True
except Exception as e:
print(f"Excel export error: {e}")
result = False
else:
# Not implemented
QMessageBox.information(
self,
"Not Implemented",
f"Export to {export_format} is not yet implemented."
)
return
if result:
self.statusBar().showMessage(f"Data exported to {file_path}", 3000)
else:
self.statusBar().showMessage("Error exporting data", 3000)
except Exception as e:
QMessageBox.critical(
self,
"Export Error",
f"Error exporting data: {str(e)}"
)
@Slot()
def show_about_dialog(self):
"""Show about dialog"""
QMessageBox.about(
self,
"About Traffic Monitoring System",
"<h3>Traffic Monitoring System</h3>"
"<p>Based on OpenVINO™ and PySide6</p>"
"<p>Version 1.0.0</p>"
"<p>© 2025 GSOC Project</p>"
)
@Slot(bool)
def toggle_video_processing(self, start):
"""
Start or stop video processing.
Args:
start: True to start processing, False to stop
"""
if self.video_controller:
if start:
try:
# Make sure the source is correctly set to what the LiveTab has
current_source = self.live_tab.current_source
print(f"DEBUG: MainWindow toggle_processing with source: {current_source} (type: {type(current_source)})")
# Validate source
if current_source is None:
self.statusBar().showMessage("Error: No valid source selected")
return
# For file sources, verify file exists
if isinstance(current_source, str) and not current_source.isdigit():
if not os.path.exists(current_source):
self.statusBar().showMessage(f"Error: File not found: {current_source}")
return
# Ensure the source is set before starting
print(f"🎥 Setting video controller source to: {current_source}")
self.video_controller.set_source(current_source)
# Now start processing after a short delay to ensure source is set
print("⏱️ Scheduling video processing start after 200ms delay...")
QTimer.singleShot(200, lambda: self._start_video_processing())
source_desc = f"file: {os.path.basename(current_source)}" if isinstance(current_source, str) and os.path.exists(current_source) else f"camera: {current_source}"
self.statusBar().showMessage(f"Video processing started with {source_desc}")
except Exception as e:
print(f"❌ Error starting video: {e}")
traceback.print_exc()
self.statusBar().showMessage(f"Error: {str(e)}")
else:
try:
print("🛑 Stopping video processing...")
self.video_controller.stop()
print("✅ Video controller stopped")
self.statusBar().showMessage("Video processing stopped")
except Exception as e:
print(f"❌ Error stopping video: {e}")
traceback.print_exc()
def _start_video_processing(self):
"""Actual video processing start with extra error handling"""
try:
print("🚀 Starting video controller...")
self.video_controller.start()
print("✅ Video controller started successfully")
except Exception as e:
print(f"❌ Error in video processing start: {e}")
traceback.print_exc()
self.statusBar().showMessage(f"Video processing error: {str(e)}")
def closeEvent(self, event):
"""Handle window close event"""
# Stop processing
if self.video_controller and self.video_controller._running:
self.video_controller.stop()
# Save settings
self.saveSettings()
# Accept close event
event.accept()
def restoreSettings(self):
"""Restore application settings"""
# Restore window geometry
geometry = self.settings.value("geometry")
if geometry:
self.restoreGeometry(geometry)
# Restore window state
state = self.settings.value("windowState")
if state:
self.restoreState(state)
def saveSettings(self):
"""Save application settings"""
# Save window geometry
self.settings.setValue("geometry", self.saveGeometry())
# Save window state
self.settings.setValue("windowState", self.saveState())
# Save current directory as snapshot directory
self.settings.setValue("snapshot_dir", os.getcwd())
@Slot(dict)
def update_traffic_light_status(self, stats):
"""Update status bar with traffic light information if detected"""
traffic_light_info = stats.get('traffic_light_color', 'unknown')
# Handle both string and dictionary return formats
if isinstance(traffic_light_info, dict):
traffic_light_color = traffic_light_info.get('color', 'unknown')
confidence = traffic_light_info.get('confidence', 0.0)
confidence_str = f" (Confidence: {confidence:.2f})" if confidence > 0 else ""
else:
traffic_light_color = traffic_light_info
confidence_str = ""
if traffic_light_color != 'unknown':
current_message = self.statusBar().currentMessage()
if not current_message or "Traffic Light" not in current_message:
# Handle both dictionary and string formats
if isinstance(traffic_light_color, dict):
color_text = traffic_light_color.get("color", "unknown").upper()
else:
color_text = str(traffic_light_color).upper()
self.statusBar().showMessage(f"Traffic Light: {color_text}{confidence_str}")
@Slot(dict)
def handle_violation_detected(self, violation):
"""Handle a detected traffic violation"""
try:
# Flash red status message
self.statusBar().showMessage(f"🚨 RED LIGHT VIOLATION DETECTED - Vehicle ID: {violation['track_id']}", 5000)
# Add to violations tab
self.violations_tab.add_violation(violation)
# Update analytics
if self.analytics_controller:
self.analytics_controller.register_violation(violation)
print(f"🚨 Violation processed: {violation['id']} at {violation['timestamp']}")
except Exception as e:
print(f"❌ Error handling violation: {e}")
import traceback
traceback.print_exc()
def _handle_video_file_selected(self, file_path):
print(f"[VideoDetection] File selected: {file_path}")
self.video_file_controller.set_source(file_path)
def _handle_video_play(self):
print("[VideoDetection] Play clicked")
self.video_file_controller.play()
def _handle_video_pause(self):
print("[VideoDetection] Pause clicked")
self.video_file_controller.pause()
def _handle_video_stop(self):
print("[VideoDetection] Stop clicked")
self.video_file_controller.stop()
def _handle_video_detection_toggle(self, enabled):
print(f"[VideoDetection] Detection toggled: {enabled}")
self.video_file_controller.set_detection_enabled(enabled)
def _handle_video_screenshot(self):
print("[VideoDetection] Screenshot clicked")
frame = self.video_file_controller.capture_snapshot()
if frame is not None:
save_dir = self.settings.value("snapshot_dir", ".")
file_path = os.path.join(save_dir, "video_snapshot_" + str(int(time.time())) + ".jpg")
saved_path = save_snapshot(frame, file_path)
if saved_path:
self.statusBar().showMessage(f"Video snapshot saved: {saved_path}", 3000)
else:
self.statusBar().showMessage("Error saving video snapshot", 3000)
else:
self.statusBar().showMessage("No frame to capture", 3000)
def _handle_video_seek(self, value):
print(f"[VideoDetection] Seek changed: {value}")
self.video_file_controller.seek(value)
@Slot(str)
def handle_device_switch(self, device):
"""Handle device switch request from config panel."""
try:
# Switch model/device using ModelManager
self.model_manager.switch_model(device=device)
# Optionally, update controllers if needed
if hasattr(self.video_controller, "on_model_switched"):
self.video_controller.on_model_switched(device)
if hasattr(self.video_file_controller, "on_model_switched"):
self.video_file_controller.on_model_switched(device)
# Emit updated device info to config panel (always as a list)
if hasattr(self.model_manager, "get_device_info"):
device_info = self.model_manager.get_device_info()
if isinstance(device_info, dict):
device_info = list(device_info.keys())
self.config_panel.update_devices_info(device_info)
self.statusBar().showMessage(f"Device switched to {device}", 2000)
except Exception as e:
print(f"Error switching device: {e}")
self.statusBar().showMessage(f"Error switching device: {e}", 3000)
@Slot(dict)
def update_performance_graphs(self, stats):
"""Update the performance graphs using the new robust widget logic."""
if not hasattr(self, 'performance_tab'):
return
print(f"[PERF DEBUG] update_performance_graphs called with: {stats}")
analytics_data = {
'real_time_data': {
'timestamps': [stats.get('frame_idx', 0)],
'inference_latency': [stats.get('inference_time', 0)],
'fps': [stats.get('fps', 0)],
'device_usage': [1 if stats.get('device', 'CPU') == 'GPU' else 0],
'resolution_width': [int(stats.get('resolution', '640x360').split('x')[0]) if 'x' in stats.get('resolution', '') else 640],
'resolution_height': [int(stats.get('resolution', '640x360').split('x')[1]) if 'x' in stats.get('resolution', '') else 360],
'device_switches': [0] if stats.get('is_device_switch', False) else [],
'resolution_changes': [0] if stats.get('is_res_change', False) else [],
},
'latency_statistics': {},
'current_metrics': {},
'system_metrics': {},
}
print(f"[PERF DEBUG] analytics_data for update_performance_data: {analytics_data}")
self.performance_tab.update_performance_data(analytics_data)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,254 @@
"""
Real-time performance graphs for inference latency analysis
Shows when latency spikes occur with different resolutions and devices
"""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QGroupBox, QTabWidget, QFrame, QSplitter
)
from PySide6.QtCore import Qt, QTimer, Signal, Slot
from PySide6.QtGui import QPainter, QPen, QBrush, QColor, QFont
import numpy as np
from collections import deque
from typing import Dict, List, Any
class RealTimeGraph(QWidget):
"""Custom widget for drawing real-time graphs"""
def __init__(self, title: str = "Graph", y_label: str = "Value", max_points: int = 300):
super().__init__()
self.title = title
self.y_label = y_label
self.max_points = max_points
# Data storage
self.x_data = deque(maxlen=max_points)
self.y_data = deque(maxlen=max_points)
self.spike_markers = deque(maxlen=max_points) # Mark spikes
self.device_markers = deque(maxlen=max_points) # Mark device changes
self.resolution_markers = deque(maxlen=max_points) # Mark resolution changes
# Graph settings
self.margin = 40
self.grid_color = QColor(60, 60, 60)
self.line_color = QColor(0, 255, 255) # Cyan
self.spike_color = QColor(255, 0, 0) # Red for spikes
self.cpu_color = QColor(100, 150, 255) # Blue for CPU
self.gpu_color = QColor(255, 150, 100) # Orange for GPU
# Auto-scaling
self.y_min = 0
self.y_max = 100
self.auto_scale = True
self.setMinimumSize(400, 200)
def add_data_point(self, x: float, y: float, is_spike: bool = False, device: str = "CPU", is_res_change: bool = False):
"""Add a new data point to the graph"""
self.x_data.append(x)
self.y_data.append(y)
self.spike_markers.append(is_spike)
self.device_markers.append(device)
self.resolution_markers.append(is_res_change)
# Auto-scale Y axis
if self.auto_scale and self.y_data:
data_max = max(self.y_data)
data_min = min(self.y_data)
padding = (data_max - data_min) * 0.1
self.y_max = data_max + padding if data_max > 0 else 100
self.y_min = max(0, data_min - padding)
self.update()
def clear_data(self):
"""Clear the graph data"""
self.x_data.clear()
self.y_data.clear()
self.spike_markers.clear()
self.device_markers.clear()
self.resolution_markers.clear()
self.update()
def paintEvent(self, event):
"""Override paint event to draw the graph"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
width = self.width()
height = self.height()
graph_width = width - 2 * self.margin
graph_height = height - 2 * self.margin
# Background
painter.fillRect(self.rect(), QColor(30, 30, 30))
# Title
painter.setPen(QColor(255, 255, 255))
painter.setFont(QFont("Arial", 12, QFont.Bold))
painter.drawText(10, 20, self.title)
# Axes
painter.setPen(QPen(QColor(200, 200, 200), 2))
painter.drawLine(self.margin, self.margin, self.margin, height - self.margin)
painter.drawLine(self.margin, height - self.margin, width - self.margin, height - self.margin)
# Grid
painter.setPen(QPen(self.grid_color, 1))
for i in range(5):
y = self.margin + (graph_height * i / 4)
painter.drawLine(self.margin, y, width - self.margin, y)
for i in range(10):
x = self.margin + (graph_width * i / 9)
painter.drawLine(x, self.margin, x, height - self.margin)
# Y-axis labels
painter.setPen(QColor(200, 200, 200))
painter.setFont(QFont("Arial", 8))
for i in range(5):
y_val = self.y_min + (self.y_max - self.y_min) * (4 - i) / 4
y_pos = self.margin + (graph_height * i / 4)
painter.drawText(5, y_pos + 5, f"{y_val:.1f}")
# X-axis label
painter.save()
painter.translate(15, height // 2)
painter.rotate(-90)
painter.drawText(-len(self.y_label) * 3, 0, self.y_label)
painter.restore()
# Data points
if len(self.x_data) >= 2 and len(self.y_data) >= 2:
points = []
spike_points = []
device_changes = []
res_changes = []
x_min = min(self.x_data) if self.x_data else 0
x_max = max(self.x_data) if self.x_data else 1
x_range = x_max - x_min if x_max > x_min else 1
for i, (x_val, y_val, is_spike, device, is_res_change) in enumerate(zip(
self.x_data, self.y_data, self.spike_markers, self.device_markers, self.resolution_markers
)):
x_screen = self.margin + (x_val - x_min) / x_range * graph_width
y_screen = height - self.margin - (y_val - self.y_min) / (self.y_max - self.y_min) * graph_height
points.append((x_screen, y_screen))
if is_spike:
spike_points.append((x_screen, y_screen))
if i > 0 and device != list(self.device_markers)[i-1]:
device_changes.append((x_screen, y_screen, device))
if is_res_change:
res_changes.append((x_screen, y_screen))
if len(points) >= 2:
painter.setPen(QPen(self.line_color, 2))
for i in range(len(points) - 1):
x1, y1 = points[i]
x2, y2 = points[i + 1]
painter.drawLine(x1, y1, x2, y2)
painter.setPen(QPen(self.spike_color, 3))
painter.setBrush(QBrush(self.spike_color))
for x, y in spike_points:
painter.drawEllipse(x - 3, y - 3, 6, 6)
for x, y, device in device_changes:
color = self.gpu_color if device == "GPU" else self.cpu_color
painter.setPen(QPen(color, 2))
painter.setBrush(QBrush(color))
painter.drawRect(x - 2, self.margin, 4, graph_height)
for x, y in res_changes:
painter.setPen(QPen(QColor(255, 167, 38), 2)) # Orange for resolution change
painter.drawLine(x, self.margin, x, height - self.margin)
class PerformanceGraphsWidget(QWidget):
def __init__(self):
super().__init__()
self.setup_ui()
self.update_timer = QTimer()
self.update_timer.timeout.connect(self.update_graphs)
try:
self.update_timer.start(1000)
except Exception as e:
print(f"❌ Error starting performance graph timer: {e}")
self.start_time = None
self.latest_data = {}
self.cpu_usage_history = deque(maxlen=300)
self.ram_usage_history = deque(maxlen=300)
def setup_ui(self):
layout = QVBoxLayout(self)
title_label = QLabel("🔥 Real-Time Inference Performance & Latency Spike Analysis")
title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #FFD700; margin: 10px;")
layout.addWidget(title_label)
self.cpu_ram_stats = QLabel("CPU: 0% | RAM: 0%")
self.cpu_ram_stats.setStyleSheet("color: #FFD700; font-weight: bold; font-size: 14px; margin: 8px;")
layout.addWidget(self.cpu_ram_stats)
splitter = QSplitter(Qt.Vertical)
# Latency graph
latency_frame = QFrame()
latency_layout = QVBoxLayout(latency_frame)
self.latency_graph = RealTimeGraph(
"Inference Latency Over Time",
"Latency (ms)",
max_points=300
)
latency_layout.addWidget(self.latency_graph)
latency_info = QHBoxLayout()
self.latency_stats = QLabel("Avg: 0ms | Max: 0ms | Spikes: 0")
self.latency_stats.setStyleSheet("color: #00FFFF; font-weight: bold;")
latency_info.addWidget(self.latency_stats)
latency_info.addStretch()
latency_layout.addLayout(latency_info)
latency_frame.setLayout(latency_layout)
splitter.addWidget(latency_frame)
# FPS graph
fps_frame = QFrame()
fps_layout = QVBoxLayout(fps_frame)
self.fps_graph = RealTimeGraph(
"FPS & Resolution Impact",
"FPS",
max_points=300
)
fps_layout.addWidget(self.fps_graph)
fps_info = QHBoxLayout()
self.fps_stats = QLabel("Current FPS: 0 | Resolution: - | Device: -")
self.fps_stats.setStyleSheet("color: #00FF00; font-weight: bold;")
fps_info.addWidget(self.fps_stats)
fps_info.addStretch()
fps_layout.addLayout(fps_info)
fps_frame.setLayout(fps_layout)
splitter.addWidget(fps_frame)
# Device switching & resolution changes graph
device_frame = QFrame()
device_layout = QVBoxLayout(device_frame)
self.device_graph = RealTimeGraph(
"Device Switching & Resolution Changes",
"-",
max_points=300
)
device_layout.addWidget(self.device_graph)
self.device_legend = QLabel("<span style='color:#ff4444;'>CPU Spikes</span>: 0 | <span style='color:#ff5722;'>GPU Spikes</span>: 0 | <span style='color:#2196f3;'>Switches</span>: 0 | <span style='color:#ffa726;'>Res Changes</span>: 0")
self.device_legend.setStyleSheet("color: #ffb300; font-size: 13px; font-weight: bold; margin: 2px 0 0 8px;")
device_layout.addWidget(self.device_legend)
device_frame.setLayout(device_layout)
splitter.addWidget(device_frame)
layout.addWidget(splitter)
self.setLayout(layout)
def update_graphs(self):
# Placeholder for updating graphs with new data
pass
def update_performance_data(self, analytics_data: Dict[str, Any]):
"""Update graphs with new analytics data, including system metrics"""
try:
print(f"[PERF DEBUG] update_performance_data called with: {analytics_data}")
chart_data = analytics_data.get('real_time_data', {})
latency_stats = analytics_data.get('latency_statistics', {})
current_metrics = analytics_data.get('current_metrics', {})
system_metrics = analytics_data.get('system_metrics', {})
if not chart_data.get('timestamps'):
print("[PERF DEBUG] No timestamps in chart_data")
return
self.latest_data = {
'chart_data': chart_data,
'latency_stats': latency_stats,
'current_metrics': current_metrics,
'system_metrics': system_metrics
}
self.update_graphs() # Immediately update graphs on new data
except Exception as e:
print(f"❌ Error updating performance data: {e}")

View File

@@ -0,0 +1,194 @@
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QLabel, QSizePolicy,
QGraphicsView, QGraphicsScene
)
from PySide6.QtCore import Qt, Signal, QSize
from PySide6.QtGui import QPixmap, QImage, QPainter
import cv2
import numpy as np
class SimpleLiveDisplay(QWidget):
"""Enhanced implementation for video display using QGraphicsView"""
video_dropped = Signal(str) # For drag and drop compatibility
def __init__(self):
super().__init__()
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
# Create QGraphicsView and QGraphicsScene
self.graphics_view = QGraphicsView()
self.graphics_scene = QGraphicsScene()
self.graphics_view.setScene(self.graphics_scene)
self.graphics_view.setMinimumSize(640, 480)
self.graphics_view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.graphics_view.setStyleSheet("background-color: black;")
self.graphics_view.setRenderHint(QPainter.Antialiasing)
self.graphics_view.setRenderHint(QPainter.SmoothPixmapTransform)
# Create backup label (in case QGraphicsView doesn't work)
self.display_label = QLabel()
self.display_label.setAlignment(Qt.AlignCenter)
self.display_label.setMinimumSize(640, 480)
self.display_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.display_label.setStyleSheet("background-color: black;")
# Set up drag and drop
self.setAcceptDrops(True)
# Add QGraphicsView to layout (primary display)
self.layout.addWidget(self.graphics_view)
# Don't add label to layout, we'll only use it as fallback if needed
def update_frame(self, pixmap):
"""Update the display with a new frame"""
if pixmap and not pixmap.isNull():
print(f"DEBUG: SimpleLiveDisplay updating with pixmap {pixmap.width()}x{pixmap.height()}")
try:
# Method 1: Using QGraphicsScene
self.graphics_scene.clear()
self.graphics_scene.addPixmap(pixmap)
self.graphics_view.fitInView(self.graphics_scene.itemsBoundingRect(), Qt.KeepAspectRatio)
# Force an immediate update
self.graphics_view.update()
self.repaint() # Force a complete repaint
print("DEBUG: SimpleLiveDisplay - pixmap displayed successfully in QGraphicsView")
except Exception as e:
print(f"ERROR in QGraphicsView display: {e}, falling back to QLabel")
try:
# Fallback method: Using QLabel
scaled_pixmap = pixmap.scaled(
self.display_label.size(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
self.display_label.setPixmap(scaled_pixmap)
self.display_label.update()
except Exception as e2:
print(f"ERROR in QLabel fallback: {e2}")
import traceback
traceback.print_exc()
else:
print("DEBUG: SimpleLiveDisplay received null or invalid pixmap")
def resizeEvent(self, event):
"""Handle resize events"""
super().resizeEvent(event)
# If we have a pixmap, rescale it to fit the new size
if not self.display_label.pixmap() or self.display_label.pixmap().isNull():
return
scaled_pixmap = self.display_label.pixmap().scaled(
self.display_label.size(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
self.display_label.setPixmap(scaled_pixmap)
def reset_display(self):
"""Reset display to black"""
blank = QPixmap(self.display_label.size())
blank.fill(Qt.black)
self.display_label.setPixmap(blank)
def dragEnterEvent(self, event):
"""Handle drag enter events"""
if event.mimeData().hasUrls():
url = event.mimeData().urls()[0].toLocalFile()
if url.lower().endswith(('.mp4', '.avi', '.mov', '.mkv', '.webm')):
event.acceptProposedAction()
def dropEvent(self, event):
"""Handle drop events"""
if event.mimeData().hasUrls():
url = event.mimeData().urls()[0].toLocalFile()
if url.lower().endswith(('.mp4', '.avi', '.mov', '.mkv', '.webm')):
self.video_dropped.emit(url)
def display_frame(self, frame: np.ndarray):
"""Display a NumPy OpenCV frame directly (converts to QPixmap and calls update_frame)"""
if frame is None:
print("⚠️ Empty frame received")
return
# Force a debug print with the frame shape
print(f"🟢 display_frame CALLED with frame: type={type(frame)}, shape={getattr(frame, 'shape', None)}")
try:
# Make a copy of the frame to ensure we're not using memory that might be released
frame_copy = frame.copy()
# Convert BGR to RGB (OpenCV uses BGR, Qt uses RGB)
rgb_frame = cv2.cvtColor(frame_copy, cv2.COLOR_BGR2RGB)
# Print shape info
h, w, ch = rgb_frame.shape
print(f"📊 Frame dimensions: {w}x{h}, channels: {ch}")
# Force continuous array for QImage
if not rgb_frame.flags['C_CONTIGUOUS']:
rgb_frame = np.ascontiguousarray(rgb_frame)
# Create QImage - critical to use .copy() to ensure Qt owns the data
bytes_per_line = ch * w
qt_image = QImage(rgb_frame.data, w, h, bytes_per_line, QImage.Format_RGB888).copy()
# Check if QImage is valid
if qt_image.isNull():
print("⚠️ Failed to create QImage")
return
# Create QPixmap from QImage
pixmap = QPixmap.fromImage(qt_image)
# METHOD 1: Display using QGraphicsScene/View
try:
self.graphics_scene.clear()
self.graphics_scene.addPixmap(pixmap)
self.graphics_view.setScene(self.graphics_scene)
# Set the view to fit the content
self.graphics_view.fitInView(self.graphics_scene.itemsBoundingRect(), Qt.KeepAspectRatio)
# Force immediate updates
self.graphics_view.viewport().update()
self.graphics_view.update()
print("✅ Frame displayed in QGraphicsView")
except Exception as e:
print(f"⚠️ Error in QGraphicsView display: {e}")
# METHOD 2: Fall back to QLabel if QGraphicsView fails
try:
# Add to layout if not already there
if self.display_label not in self.layout.children():
self.layout.addWidget(self.display_label)
self.graphics_view.hide()
self.display_label.show()
# Scale pixmap for display
scaled_pixmap = pixmap.scaled(
max(self.display_label.width(), 640),
max(self.display_label.height(), 480),
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
self.display_label.setPixmap(scaled_pixmap)
self.display_label.setScaledContents(True)
self.display_label.update()
print("✅ Frame displayed in QLabel (fallback)")
except Exception as e2:
print(f"❌ ERROR in QLabel fallback: {e2}")
import traceback
traceback.print_exc()
except Exception as main_error:
print(f"❌ CRITICAL ERROR in display_frame: {main_error}")
import traceback
traceback.print_exc()

View File

@@ -0,0 +1,87 @@
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QLabel, QSizePolicy
)
from PySide6.QtCore import Qt, Signal, QSize
from PySide6.QtGui import QPixmap, QImage
import cv2
import numpy as np
class SimpleLiveDisplay(QWidget):
"""Simpler implementation for video display using QLabel instead of QGraphicsView"""
video_dropped = Signal(str) # For drag and drop compatibility
def __init__(self):
super().__init__()
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
# Create QLabel for display
self.display_label = QLabel()
self.display_label.setAlignment(Qt.AlignCenter)
self.display_label.setMinimumSize(640, 480)
self.display_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.display_label.setStyleSheet("background-color: black;")
# Set up drag and drop
self.setAcceptDrops(True)
# Add to layout
self.layout.addWidget(self.display_label)
def update_frame(self, pixmap):
"""Update the display with a new frame"""
if pixmap and not pixmap.isNull():
print(f"DEBUG: SimpleLiveDisplay updating with pixmap {pixmap.width()}x{pixmap.height()}")
try:
# Try direct approach - set the pixmap directly without scaling
self.display_label.setPixmap(pixmap)
# Force an immediate update
self.display_label.update()
self.repaint() # Force a complete repaint
print("DEBUG: SimpleLiveDisplay - pixmap set successfully")
except Exception as e:
print(f"ERROR in SimpleLiveDisplay.update_frame: {e}")
import traceback
traceback.print_exc()
else:
print("DEBUG: SimpleLiveDisplay received null or invalid pixmap")
def resizeEvent(self, event):
"""Handle resize events"""
super().resizeEvent(event)
# If we have a pixmap, rescale it to fit the new size
if not self.display_label.pixmap() or self.display_label.pixmap().isNull():
return
scaled_pixmap = self.display_label.pixmap().scaled(
self.display_label.size(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
self.display_label.setPixmap(scaled_pixmap)
def reset_display(self):
"""Reset display to black"""
blank = QPixmap(self.display_label.size())
blank.fill(Qt.black)
self.display_label.setPixmap(blank)
def dragEnterEvent(self, event):
"""Handle drag enter events"""
if event.mimeData().hasUrls():
url = event.mimeData().urls()[0].toLocalFile()
if url.lower().endswith(('.mp4', '.avi', '.mov', '.mkv', '.webm')):
event.acceptProposedAction()
def dropEvent(self, event):
"""Handle drop events"""
if event.mimeData().hasUrls():
url = event.mimeData().urls()[0].toLocalFile()
if url.lower().endswith(('.mp4', '.avi', '.mov', '.mkv', '.webm')):
self.video_dropped.emit(url)

View File

@@ -0,0 +1,111 @@
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QLabel, QSizePolicy
)
from PySide6.QtCore import Qt, Signal, QSize
from PySide6.QtGui import QPixmap, QImage
import cv2
import numpy as np
class SimpleLiveDisplay(QWidget):
"""Simpler implementation for video display using QLabel instead of QGraphicsView"""
video_dropped = Signal(str) # For drag and drop compatibility
def __init__(self):
super().__init__()
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
# Create QLabel for display
self.display_label = QLabel()
self.display_label.setAlignment(Qt.AlignCenter)
self.display_label.setMinimumSize(640, 480)
self.display_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.display_label.setStyleSheet("background-color: black;")
# Set up drag and drop
self.setAcceptDrops(True)
# Add to layout
self.layout.addWidget(self.display_label)
# Initialize with black screen
self.reset_display()
def update_frame(self, pixmap):
"""Update the display with a new frame"""
if pixmap and not pixmap.isNull():
print(f"DEBUG: SimpleLiveDisplay updating with pixmap {pixmap.width()}x{pixmap.height()}")
try:
# Get current label size
label_size = self.display_label.size()
if label_size.width() <= 1 or label_size.height() <= 1:
# If label doesn't have valid size yet, use a reasonable default
label_size = QSize(640, 480)
# Make a deep copy to prevent any sharing issues
pixmap_copy = QPixmap(pixmap)
# Scale the pixmap to fit the label while maintaining aspect ratio
scaled_pixmap = pixmap_copy.scaled(
label_size,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation
)
# Set the pixmap to the label
self.display_label.setPixmap(scaled_pixmap)
# Force an immediate update
self.display_label.update()
print("DEBUG: SimpleLiveDisplay - pixmap set successfully")
except Exception as e:
print(f"ERROR in SimpleLiveDisplay.update_frame: {e}")
import traceback
traceback.print_exc()
else:
print("DEBUG: SimpleLiveDisplay received null or invalid pixmap")
def resizeEvent(self, event):
"""Handle resize events"""
super().resizeEvent(event)
# If we have a pixmap, rescale it to fit the new size
if not self.display_label.pixmap() or self.display_label.pixmap().isNull():
return
try:
scaled_pixmap = self.display_label.pixmap().scaled(
self.display_label.size(),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation
)
self.display_label.setPixmap(scaled_pixmap)
except Exception as e:
print(f"ERROR in SimpleLiveDisplay.resizeEvent: {e}")
def reset_display(self):
"""Reset display to black"""
try:
blank = QPixmap(640, 480)
blank.fill(Qt.black)
self.display_label.setPixmap(blank)
except Exception as e:
print(f"ERROR in SimpleLiveDisplay.reset_display: {e}")
def dragEnterEvent(self, event):
"""Handle drag enter events"""
if event.mimeData().hasUrls():
url = event.mimeData().urls()[0].toLocalFile()
if url.lower().endswith(('.mp4', '.avi', '.mov', '.mkv', '.webm')):
event.acceptProposedAction()
def dropEvent(self, event):
"""Handle drop events"""
if event.mimeData().hasUrls():
url = event.mimeData().urls()[0].toLocalFile()
if url.lower().endswith(('.mp4', '.avi', '.mov', '.mkv', '.webm')):
self.video_dropped.emit(url)

View File

@@ -0,0 +1,254 @@
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QSlider, QCheckBox, QFileDialog, QSizePolicy, QGridLayout, QFrame, QSpacerItem
from PySide6.QtCore import Signal, Qt
from PySide6.QtGui import QPixmap, QIcon, QFont
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() # New signal for auto model/device selection
def __init__(self):
super().__init__()
self.video_loaded = False
grid = QGridLayout(self)
grid.setContentsMargins(32, 24, 32, 24)
grid.setSpacing(0)
# File select bar (top)
file_bar = QHBoxLayout()
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)
file_bar.addWidget(self.file_btn)
file_bar.addWidget(self.file_label)
file_bar.addStretch(1)
# Video display area (centered, scalable)
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)
# Diagnostic overlay (now below video, not over it)
self.overlay = DiagnosticOverlay()
self.overlay.setStyleSheet(self.overlay.styleSheet() + "border: 1px solid #03DAC5;")
self.overlay.setFixedHeight(90)
# FPS and Inference badges (below video)
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)
# Horizontal layout for overlay and badges
self.badge_bar = QHBoxLayout()
self.badge_bar.setContentsMargins(0, 8, 0, 8)
self.badge_bar.addWidget(self.fps_badge)
self.badge_bar.addSpacing(12)
self.badge_bar.addWidget(self.inference_badge)
self.badge_bar.addSpacing(18)
self.badge_bar.addWidget(self.overlay) # Overlay will stretch to fill right side
self.badge_bar.addStretch(10)
video_layout.addStretch(1) # Push badge bar to the bottom
video_layout.addLayout(self.badge_bar)
# Control bar (bottom)
control_bar = QHBoxLayout()
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(1)
# Layout grid
grid.addLayout(file_bar, 0, 0, 1, 1)
grid.addWidget(video_frame, 1, 0, 1, 1)
grid.addLayout(self.badge_bar, 2, 0, 1, 1)
grid.addLayout(control_bar, 3, 0, 1, 1)
grid.setRowStretch(1, 1)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
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() # Request auto model/device selection
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() # Also trigger auto-select when controls are enabled
def update_display(self, pixmap):
# Maintain aspect ratio
if pixmap:
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):
# Accepts a stats dict for extensibility
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)
fps = stats.get('fps', None)
# Try all possible keys for inference time
inference = stats.get('inference', stats.get('detection_time', stats.get('detection_time_ms', None)))
model = stats.get('model', stats.get('model_name', '-'))
device = stats.get('device', stats.get('device_name', '-'))
# Update overlay
self.overlay.update_overlay(model, device, cars, trucks, peds, tlights, motorcycles)
# Update FPS and Inference badges
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")
def update_progress(self, value, max_value, timestamp):
self.progress.setMaximum(max_value)
self.progress.setValue(value)
# Format timestamp as string (e.g., "00:00 / 00:00" or just str)
if isinstance(timestamp, float) or isinstance(timestamp, int):
timestamp_str = f"{timestamp:.2f}"
else:
timestamp_str = str(timestamp)
self.timestamp.setText(timestamp_str)

View File

@@ -0,0 +1,361 @@
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem,
QLineEdit, QLabel, QPushButton, QSplitter, QHeaderView,
QComboBox, QGroupBox, QFormLayout
)
from PySide6.QtCore import Qt, Slot
from PySide6.QtGui import QPixmap, QColor
from datetime import datetime
import os
class ViolationsTab(QWidget):
"""Tab for displaying and managing traffic violations."""
def __init__(self):
super().__init__()
self.initUI()
self.violations_data = []
def initUI(self):
"""Initialize UI components"""
layout = QVBoxLayout(self)
# Add status label for violations
self.status_label = QLabel("🟢 Red Light Violation Detection Active")
self.status_label.setStyleSheet("font-size: 16px; color: #22AA22; font-weight: bold; padding: 10px;")
self.status_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.status_label)
# Search and filter controls
filter_layout = QHBoxLayout()
self.search_box = QLineEdit()
self.search_box.setPlaceholderText("Search violations...")
self.search_box.textChanged.connect(self.filter_violations)
self.filter_combo = QComboBox()
self.filter_combo.addItem("All Types")
self.filter_combo.addItem("Red Light")
self.filter_combo.addItem("Stop Sign")
self.filter_combo.addItem("Speed")
self.filter_combo.addItem("Lane")
self.filter_combo.currentTextChanged.connect(self.filter_violations)
filter_layout.addWidget(QLabel("Filter:"))
filter_layout.addWidget(self.filter_combo)
filter_layout.addStretch(1)
filter_layout.addWidget(QLabel("Search:"))
filter_layout.addWidget(self.search_box)
layout.addLayout(filter_layout)
# Splitter for table and details
splitter = QSplitter(Qt.Horizontal)
# Violations table
self.table = QTableWidget(0, 5)
self.table.setHorizontalHeaderLabels(["ID", "Type", "Timestamp", "Details", "Vehicle"])
self.table.setSelectionBehavior(QTableWidget.SelectRows)
self.table.setSelectionMode(QTableWidget.SingleSelection)
self.table.setEditTriggers(QTableWidget.NoEditTriggers)
self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.Stretch)
self.table.verticalHeader().setVisible(False)
self.table.setAlternatingRowColors(True)
self.table.setStyleSheet("alternate-background-color: rgba(240, 240, 240, 100);")
self.table.selectionModel().selectionChanged.connect(self.on_violation_selected)
splitter.addWidget(self.table)
# Violation details panel
details_panel = QWidget()
details_layout = QVBoxLayout(details_panel)
# Violation info
info_group = QGroupBox("Violation Details")
info_layout = QFormLayout(info_group)
self.violation_type_label = QLabel("--")
self.violation_time_label = QLabel("--")
self.violation_details_label = QLabel("--")
self.violation_vehicle_label = QLabel("--")
self.violation_location_label = QLabel("--")
info_layout.addRow("Type:", self.violation_type_label)
info_layout.addRow("Time:", self.violation_time_label)
info_layout.addRow("Details:", self.violation_details_label)
info_layout.addRow("Vehicle ID:", self.violation_vehicle_label)
info_layout.addRow("Location:", self.violation_location_label)
details_layout.addWidget(info_group)
# Snapshot preview
snapshot_group = QGroupBox("Violation Snapshot")
snapshot_layout = QVBoxLayout(snapshot_group)
self.preview_label = QLabel()
self.preview_label.setAlignment(Qt.AlignCenter)
self.preview_label.setMinimumSize(320, 240)
self.preview_label.setStyleSheet("background-color: #222; border: 1px solid #444;")
snapshot_layout.addWidget(self.preview_label)
details_layout.addWidget(snapshot_group)
# Actions
actions_layout = QHBoxLayout()
self.export_btn = QPushButton("Export Report")
self.dismiss_btn = QPushButton("Dismiss")
actions_layout.addWidget(self.export_btn)
actions_layout.addWidget(self.dismiss_btn)
details_layout.addLayout(actions_layout)
details_layout.addStretch(1)
splitter.addWidget(details_panel)
splitter.setSizes([600, 400]) # Initial sizes
layout.addWidget(splitter)
# Status bar
status_layout = QHBoxLayout()
self.status_label = QLabel("No violations recorded")
status_layout.addWidget(self.status_label)
self.clear_btn = QPushButton("Clear All")
status_layout.addWidget(self.clear_btn)
layout.addLayout(status_layout)
@Slot()
def filter_violations(self):
"""Filter violations based on search text and type filter"""
search_text = self.search_box.text().lower()
filter_type = self.filter_combo.currentText()
self.table.setRowCount(0)
filtered_count = 0
for violation in self.violations_data:
# Filter by type
if filter_type != "All Types":
violation_type = violation.get('type', '').lower()
filter_match = filter_type.lower() in violation_type
if not filter_match:
continue
# Filter by search text
if search_text:
# Search in multiple fields
searchable_text = (
violation.get('type', '').lower() + ' ' +
violation.get('details', '').lower() + ' ' +
str(violation.get('vehicle_id', '')).lower() + ' ' +
str(violation.get('timestamp_str', '')).lower()
)
if search_text not in searchable_text:
continue
# Add row for matching violation
row_position = self.table.rowCount()
self.table.insertRow(row_position)
# Create violation ID
violation_id = violation.get('id', filtered_count + 1)
self.table.setItem(row_position, 0, QTableWidgetItem(str(violation_id)))
# Format violation type
violation_type = violation.get('type', '').replace('_', ' ').title()
type_item = QTableWidgetItem(violation_type)
# Color-code by violation type
if 'red light' in violation_type.lower():
type_item.setForeground(QColor(255, 0, 0))
elif 'stop sign' in violation_type.lower():
type_item.setForeground(QColor(255, 140, 0))
elif 'speed' in violation_type.lower():
type_item.setForeground(QColor(0, 0, 255))
self.table.setItem(row_position, 1, type_item)
# Format timestamp
timestamp = violation.get('timestamp', 0)
if isinstance(timestamp, (int, float)):
timestamp_str = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
violation['timestamp_str'] = timestamp_str # Store for search
else:
timestamp_str = str(timestamp)
self.table.setItem(row_position, 2, QTableWidgetItem(timestamp_str))
# Details
self.table.setItem(row_position, 3, QTableWidgetItem(violation.get('details', '')))
# Vehicle ID
self.table.setItem(row_position, 4, QTableWidgetItem(str(violation.get('vehicle_id', ''))))
filtered_count += 1
# Update status
self.status_label.setText(f"Showing {filtered_count} of {len(self.violations_data)} violations")
@Slot()
def on_violation_selected(self):
"""Handle violation selection in table"""
selected_items = self.table.selectedItems()
if not selected_items:
return
row = selected_items[0].row()
violation_id = int(self.table.item(row, 0).text())
# Find violation in data
violation = None
for v in self.violations_data:
if v.get('id', -1) == violation_id:
violation = v
break
if not violation:
return
# Update details panel with enhanced information
violation_type = violation.get('violation_type', 'red_light').replace('_', ' ').title()
# Add traffic light confidence if available
traffic_light_info = violation.get('traffic_light', {})
if isinstance(traffic_light_info, dict) and 'confidence' in traffic_light_info:
tl_color = traffic_light_info.get('color', 'red').upper()
tl_confidence = traffic_light_info.get('confidence', 0.0)
violation_type = f"{violation_type} - {tl_color} ({tl_confidence:.2f})"
self.violation_type_label.setText(violation_type)
# Format timestamp
timestamp = violation.get('timestamp', 0)
if isinstance(timestamp, (int, float)):
timestamp_str = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
else:
timestamp_str = str(timestamp)
self.violation_time_label.setText(timestamp_str)
# Add vehicle details with confidence
vehicle_type = violation.get('vehicle_type', 'Unknown').capitalize()
vehicle_confidence = violation.get('confidence', 0.0)
details = f"{vehicle_type} (Conf: {vehicle_confidence:.2f})"
self.violation_details_label.setText(details)
self.violation_vehicle_label.setText(str(violation.get('track_id', '--')))
# Format location
if 'bbox' in violation:
bbox = violation['bbox']
loc_str = f"X: {int(bbox[0])}, Y: {int(bbox[1])}"
else:
loc_str = "Unknown"
self.violation_location_label.setText(loc_str)
# Update snapshot if available
if 'snapshot' in violation and violation['snapshot'] is not None:
self.preview_label.setPixmap(QPixmap(violation['snapshot']))
else:
self.preview_label.setText("No snapshot available")
@Slot(list)
def update_violations(self, violations):
"""
Update violations list.
Args:
violations: List of violation dictionaries
"""
# Store violations data
for violation in violations:
# Check if already in list (by timestamp and vehicle ID)
is_duplicate = False
for existing in self.violations_data:
if (existing.get('timestamp') == violation.get('timestamp') and
existing.get('vehicle_id') == violation.get('vehicle_id')):
is_duplicate = True
break
if not is_duplicate:
# Assign ID
violation['id'] = len(self.violations_data) + 1
self.violations_data.append(violation)
# Refresh display
self.filter_violations()
def clear_all_violations(self):
"""Clear all violation data"""
self.violations_data = []
self.table.setRowCount(0)
self.status_label.setText("No violations recorded")
# Clear details
self.violation_type_label.setText("--")
self.violation_time_label.setText("--")
self.violation_details_label.setText("--")
self.violation_vehicle_label.setText("--")
self.violation_location_label.setText("--")
self.preview_label.clear()
self.preview_label.setText("No violation selected")
@Slot(object)
def add_violation(self, violation):
"""
Add a new violation to the table.
Args:
violation: Dictionary with violation information
"""
try:
# Update status to show active violations
self.status_label.setText(f"🚨 RED LIGHT VIOLATION DETECTED - Total: {len(self.violations_data) + 1}")
self.status_label.setStyleSheet("font-size: 16px; color: #FF2222; font-weight: bold; padding: 10px;")
# Add to violations data
self.violations_data.append(violation)
# Add to table
row = self.table.rowCount()
self.table.insertRow(row)
# Format timestamp
timestamp_str = violation['timestamp'].strftime("%Y-%m-%d %H:%M:%S")
# Set table items with enhanced information
self.table.setItem(row, 0, QTableWidgetItem(str(violation['id'])))
# Check for traffic light confidence information
traffic_light_info = violation.get('traffic_light', {})
if traffic_light_info and isinstance(traffic_light_info, dict):
tl_confidence = traffic_light_info.get('confidence', 0.0)
violation_type = f"Red Light ({tl_confidence:.2f})"
else:
violation_type = "Red Light"
self.table.setItem(row, 1, QTableWidgetItem(violation_type))
self.table.setItem(row, 2, QTableWidgetItem(timestamp_str))
# Add vehicle type and detection confidence
vehicle_type = violation.get('vehicle_type', 'Unknown').capitalize()
self.table.setItem(row, 3, QTableWidgetItem(f"{vehicle_type}"))
self.table.setItem(row, 4, QTableWidgetItem(f"{violation.get('confidence', 0.0):.2f}"))
# Highlight new row
for col in range(5):
item = self.table.item(row, col)
if item:
item.setBackground(QColor(255, 200, 200))
# Load snapshot if available
if violation.get('snapshot_path') and os.path.exists(violation['snapshot_path']):
pixmap = QPixmap(violation['snapshot_path'])
if not pixmap.isNull():
# Store reference to avoid garbage collection
violation['pixmap'] = pixmap
except Exception as e:
print(f"❌ Error adding violation to UI: {e}")
import traceback
traceback.print_exc()