Clean push: Removed heavy files & added only latest snapshot
This commit is contained in:
1576
qt_app_pyside1/ui/UI.py
Normal file
1576
qt_app_pyside1/ui/UI.py
Normal file
File diff suppressed because it is too large
Load Diff
1
qt_app_pyside1/ui/__init__.py
Normal file
1
qt_app_pyside1/ui/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# UI package for Traffic Monitoring System
|
||||
BIN
qt_app_pyside1/ui/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
qt_app_pyside1/ui/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
qt_app_pyside1/ui/__pycache__/analytics_tab.cpython-311.pyc
Normal file
BIN
qt_app_pyside1/ui/__pycache__/analytics_tab.cpython-311.pyc
Normal file
Binary file not shown.
BIN
qt_app_pyside1/ui/__pycache__/config_panel.cpython-311.pyc
Normal file
BIN
qt_app_pyside1/ui/__pycache__/config_panel.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
qt_app_pyside1/ui/__pycache__/export_tab.cpython-311.pyc
Normal file
BIN
qt_app_pyside1/ui/__pycache__/export_tab.cpython-311.pyc
Normal file
Binary file not shown.
BIN
qt_app_pyside1/ui/__pycache__/fixed_live_tab.cpython-311.pyc
Normal file
BIN
qt_app_pyside1/ui/__pycache__/fixed_live_tab.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
qt_app_pyside1/ui/__pycache__/live_multi_cam_tab.cpython-311.pyc
Normal file
BIN
qt_app_pyside1/ui/__pycache__/live_multi_cam_tab.cpython-311.pyc
Normal file
Binary file not shown.
BIN
qt_app_pyside1/ui/__pycache__/main_window.cpython-311.pyc
Normal file
BIN
qt_app_pyside1/ui/__pycache__/main_window.cpython-311.pyc
Normal file
Binary file not shown.
BIN
qt_app_pyside1/ui/__pycache__/main_window1.cpython-311.pyc
Normal file
BIN
qt_app_pyside1/ui/__pycache__/main_window1.cpython-311.pyc
Normal file
Binary file not shown.
BIN
qt_app_pyside1/ui/__pycache__/performance_graphs.cpython-311.pyc
Normal file
BIN
qt_app_pyside1/ui/__pycache__/performance_graphs.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
qt_app_pyside1/ui/__pycache__/violations_tab.cpython-311.pyc
Normal file
BIN
qt_app_pyside1/ui/__pycache__/violations_tab.cpython-311.pyc
Normal file
Binary file not shown.
662
qt_app_pyside1/ui/analytics_tab.py
Normal file
662
qt_app_pyside1/ui/analytics_tab.py
Normal 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}")
|
||||
666
qt_app_pyside1/ui/config_panel.py
Normal file
666
qt_app_pyside1/ui/config_panel.py
Normal 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" <span style='color:#b2dfdb;'>{k}</span>: <span style='color:#fff'>{v}</span><br>"
|
||||
else:
|
||||
text += " <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
|
||||
208
qt_app_pyside1/ui/enhanced_simple_live_display.py
Normal file
208
qt_app_pyside1/ui/enhanced_simple_live_display.py
Normal 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()
|
||||
360
qt_app_pyside1/ui/export_tab.py
Normal file
360
qt_app_pyside1/ui/export_tab.py
Normal 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
|
||||
361
qt_app_pyside1/ui/fixed_live_tab.py
Normal file
361
qt_app_pyside1/ui/fixed_live_tab.py
Normal 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)}")
|
||||
25
qt_app_pyside1/ui/global_status_panel.py
Normal file
25
qt_app_pyside1/ui/global_status_panel.py
Normal 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}")
|
||||
168
qt_app_pyside1/ui/live_multi_cam_tab.py
Normal file
168
qt_app_pyside1/ui/live_multi_cam_tab.py
Normal 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)
|
||||
283
qt_app_pyside1/ui/live_tab.py
Normal file
283
qt_app_pyside1/ui/live_tab.py
Normal 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")
|
||||
750
qt_app_pyside1/ui/main_window.py
Normal file
750
qt_app_pyside1/ui/main_window.py
Normal 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)
|
||||
1200
qt_app_pyside1/ui/main_window1.py
Normal file
1200
qt_app_pyside1/ui/main_window1.py
Normal file
File diff suppressed because it is too large
Load Diff
254
qt_app_pyside1/ui/performance_graphs.py
Normal file
254
qt_app_pyside1/ui/performance_graphs.py
Normal 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}")
|
||||
194
qt_app_pyside1/ui/simple_live_display.py
Normal file
194
qt_app_pyside1/ui/simple_live_display.py
Normal 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()
|
||||
87
qt_app_pyside1/ui/temp_live_display.py
Normal file
87
qt_app_pyside1/ui/temp_live_display.py
Normal 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)
|
||||
111
qt_app_pyside1/ui/temp_live_display.py.new
Normal file
111
qt_app_pyside1/ui/temp_live_display.py.new
Normal 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)
|
||||
254
qt_app_pyside1/ui/video_detection_tab.py
Normal file
254
qt_app_pyside1/ui/video_detection_tab.py
Normal 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)
|
||||
361
qt_app_pyside1/ui/violations_tab.py
Normal file
361
qt_app_pyside1/ui/violations_tab.py
Normal 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()
|
||||
Reference in New Issue
Block a user