646 lines
20 KiB
Python
646 lines
20 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
AI监控系统 PyQt6图形界面
|
||
连接WebSocket服务,实时显示监控画面和告警信息
|
||
"""
|
||
|
||
import sys
|
||
import json
|
||
import asyncio
|
||
import threading
|
||
import base64
|
||
from datetime import datetime
|
||
from PyQt6.QtWidgets import *
|
||
from PyQt6.QtCore import *
|
||
from PyQt6.QtGui import *
|
||
import websockets
|
||
|
||
|
||
class WebSocketWorker(QThread):
|
||
"""WebSocket连接工作线程"""
|
||
message_received = pyqtSignal(dict)
|
||
connection_status = pyqtSignal(bool, str)
|
||
|
||
def __init__(self, url):
|
||
super().__init__()
|
||
self.url = url
|
||
self.running = False
|
||
self.websocket = None
|
||
|
||
def run(self):
|
||
"""运行WebSocket连接"""
|
||
loop = asyncio.new_event_loop()
|
||
asyncio.set_event_loop(loop)
|
||
|
||
try:
|
||
self.running = True
|
||
loop.run_until_complete(self.connect_websocket())
|
||
except Exception as e:
|
||
self.connection_status.emit(False, f"连接失败: {str(e)}")
|
||
finally:
|
||
loop.close()
|
||
|
||
async def connect_websocket(self):
|
||
"""连接WebSocket服务器"""
|
||
try:
|
||
async with websockets.connect(self.url) as websocket:
|
||
self.websocket = websocket
|
||
self.connection_status.emit(True, "连接成功")
|
||
|
||
while self.running:
|
||
try:
|
||
message = await websocket.recv()
|
||
data = json.loads(message)
|
||
self.message_received.emit(data)
|
||
except websockets.exceptions.ConnectionClosed:
|
||
break
|
||
except Exception as e:
|
||
print(f"接收消息错误: {e}")
|
||
break
|
||
|
||
except Exception as e:
|
||
self.connection_status.emit(False, f"连接错误: {str(e)}")
|
||
|
||
def stop(self):
|
||
"""停止WebSocket连接"""
|
||
self.running = False
|
||
if self.websocket:
|
||
self.websocket.close()
|
||
|
||
|
||
class VideoWidget(QLabel):
|
||
"""视频显示控件"""
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.setMinimumSize(640, 480)
|
||
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
self.setStyleSheet("""
|
||
QLabel {
|
||
background-color: #2b2b2b;
|
||
border: 2px solid #555;
|
||
border-radius: 8px;
|
||
color: #888;
|
||
font-size: 14px;
|
||
}
|
||
""")
|
||
self.setText("等待视频流...")
|
||
|
||
def update_image(self, base64_data):
|
||
"""更新显示的图像"""
|
||
try:
|
||
# 解码base64图像数据
|
||
image_data = base64.b64decode(base64_data)
|
||
pixmap = QPixmap()
|
||
pixmap.loadFromData(image_data)
|
||
|
||
# 缩放图像以适应控件大小
|
||
scaled_pixmap = pixmap.scaled(
|
||
self.size(),
|
||
Qt.AspectRatioMode.KeepAspectRatio,
|
||
Qt.TransformationMode.SmoothTransformation
|
||
)
|
||
self.setPixmap(scaled_pixmap)
|
||
|
||
except Exception as e:
|
||
print(f"图像显示错误: {e}")
|
||
self.setText("图像解码失败")
|
||
|
||
|
||
class AlertListWidget(QListWidget):
|
||
"""告警列表控件"""
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.setMaximumHeight(200)
|
||
self.setStyleSheet("""
|
||
QListWidget {
|
||
background-color: #1e1e1e;
|
||
border: 1px solid #444;
|
||
border-radius: 4px;
|
||
color: white;
|
||
font-family: 'Courier New', monospace;
|
||
}
|
||
QListWidget::item {
|
||
padding: 8px;
|
||
border-bottom: 1px solid #333;
|
||
}
|
||
QListWidget::item:selected {
|
||
background-color: #0078d4;
|
||
}
|
||
""")
|
||
|
||
def add_alert(self, alert_data):
|
||
"""添加告警信息"""
|
||
timestamp = datetime.fromtimestamp(alert_data['timestamp']).strftime("%H:%M:%S")
|
||
camera_id = alert_data['camera_id']
|
||
event_type = alert_data['event_type']
|
||
video_file = alert_data.get('video_file', '')
|
||
|
||
# 创建告警项目
|
||
item_text = f"[{timestamp}] 摄像头{camera_id} - 事件类型{event_type}"
|
||
|
||
item = QListWidgetItem(item_text)
|
||
item.setData(Qt.ItemDataRole.UserRole, alert_data)
|
||
|
||
# 根据事件类型设置颜色
|
||
if event_type == 0:
|
||
item.setForeground(QColor("#4CAF50")) # 绿色 - 正常
|
||
elif event_type == 1:
|
||
item.setForeground(QColor("#FF9800")) # 橙色 - 警告
|
||
else:
|
||
item.setForeground(QColor("#F44336")) # 红色 - 严重告警
|
||
|
||
self.insertItem(0, item) # 插入到顶部
|
||
|
||
# 限制列表长度
|
||
if self.count() > 100:
|
||
self.takeItem(self.count() - 1)
|
||
|
||
|
||
class AIMonitorGUI(QMainWindow):
|
||
"""AI监控系统主界面"""
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.websocket_worker = None
|
||
self.camera_widgets = {} # 存储各摄像头的视频控件
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
"""初始化用户界面"""
|
||
self.setWindowTitle("AI监控系统 v1.0 (PyQt6)")
|
||
self.setGeometry(100, 100, 1400, 900)
|
||
|
||
# 设置深色主题
|
||
self.setStyleSheet("""
|
||
QMainWindow {
|
||
background-color: #1e1e1e;
|
||
color: white;
|
||
}
|
||
QMenuBar {
|
||
background-color: #2d2d2d;
|
||
border-bottom: 1px solid #444;
|
||
}
|
||
QMenu {
|
||
background-color: #2d2d2d;
|
||
border: 1px solid #444;
|
||
}
|
||
QStatusBar {
|
||
background-color: #2d2d2d;
|
||
border-top: 1px solid #444;
|
||
color: #ccc;
|
||
}
|
||
QPushButton {
|
||
background-color: #0078d4;
|
||
border: none;
|
||
color: white;
|
||
padding: 8px 16px;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #106ebe;
|
||
}
|
||
QPushButton:pressed {
|
||
background-color: #005a9e;
|
||
}
|
||
QPushButton:disabled {
|
||
background-color: #666;
|
||
color: #999;
|
||
}
|
||
QLabel {
|
||
color: white;
|
||
}
|
||
""")
|
||
|
||
# 创建菜单栏
|
||
self.create_menu_bar()
|
||
|
||
# 创建中央控件
|
||
central_widget = QWidget()
|
||
self.setCentralWidget(central_widget)
|
||
|
||
# 主布局
|
||
main_layout = QVBoxLayout(central_widget)
|
||
|
||
# 顶部控制栏
|
||
control_layout = self.create_control_bar()
|
||
main_layout.addLayout(control_layout)
|
||
|
||
# 中间内容区域
|
||
content_layout = QHBoxLayout()
|
||
|
||
# 左侧:摄像头视频区域
|
||
self.video_area = self.create_video_area()
|
||
content_layout.addWidget(self.video_area, 2)
|
||
|
||
# 右侧:信息面板
|
||
info_panel = self.create_info_panel()
|
||
content_layout.addWidget(info_panel, 1)
|
||
|
||
main_layout.addLayout(content_layout)
|
||
|
||
# 底部:告警列表
|
||
alert_layout = self.create_alert_section()
|
||
main_layout.addLayout(alert_layout)
|
||
|
||
# 创建状态栏
|
||
self.statusBar().showMessage("就绪")
|
||
|
||
# 自动连接WebSocket
|
||
QTimer.singleShot(1000, self.connect_websocket)
|
||
|
||
def create_menu_bar(self):
|
||
"""创建菜单栏"""
|
||
menubar = self.menuBar()
|
||
|
||
# 文件菜单
|
||
file_menu = menubar.addMenu('文件')
|
||
|
||
connect_action = QAction('连接服务器', self)
|
||
connect_action.setShortcut(QKeySequence.StandardKey.Open)
|
||
connect_action.triggered.connect(self.connect_websocket)
|
||
file_menu.addAction(connect_action)
|
||
|
||
disconnect_action = QAction('断开连接', self)
|
||
disconnect_action.setShortcut(QKeySequence.StandardKey.Close)
|
||
disconnect_action.triggered.connect(self.disconnect_websocket)
|
||
file_menu.addAction(disconnect_action)
|
||
|
||
file_menu.addSeparator()
|
||
|
||
exit_action = QAction('退出', self)
|
||
exit_action.setShortcut(QKeySequence.StandardKey.Quit)
|
||
exit_action.triggered.connect(self.close)
|
||
file_menu.addAction(exit_action)
|
||
|
||
# 帮助菜单
|
||
help_menu = menubar.addMenu('帮助')
|
||
about_action = QAction('关于', self)
|
||
about_action.triggered.connect(self.show_about)
|
||
help_menu.addAction(about_action)
|
||
|
||
def create_control_bar(self):
|
||
"""创建顶部控制栏"""
|
||
layout = QHBoxLayout()
|
||
|
||
# 连接状态指示器
|
||
self.status_label = QLabel("未连接")
|
||
self.status_label.setStyleSheet("""
|
||
QLabel {
|
||
padding: 6px 12px;
|
||
background-color: #666;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
}
|
||
""")
|
||
layout.addWidget(self.status_label)
|
||
|
||
# 连接按钮
|
||
self.connect_btn = QPushButton("连接服务器")
|
||
self.connect_btn.clicked.connect(self.toggle_connection)
|
||
layout.addWidget(self.connect_btn)
|
||
|
||
# 刷新按钮
|
||
refresh_btn = QPushButton("刷新")
|
||
refresh_btn.clicked.connect(self.refresh_videos)
|
||
layout.addWidget(refresh_btn)
|
||
|
||
# 间隔
|
||
layout.addStretch()
|
||
|
||
# 当前时间
|
||
self.time_label = QLabel()
|
||
self.time_label.setStyleSheet("color: #ccc;")
|
||
layout.addWidget(self.time_label)
|
||
|
||
# 更新时间定时器
|
||
self.timer = QTimer()
|
||
self.timer.timeout.connect(self.update_time)
|
||
self.timer.start(1000)
|
||
self.update_time()
|
||
|
||
return layout
|
||
|
||
def create_video_area(self):
|
||
"""创建视频显示区域"""
|
||
scroll_area = QScrollArea()
|
||
scroll_area.setWidgetResizable(True)
|
||
scroll_area.setStyleSheet("""
|
||
QScrollArea {
|
||
border: 1px solid #444;
|
||
background-color: #2d2d2d;
|
||
border-radius: 8px;
|
||
}
|
||
""")
|
||
|
||
# 视频网格容器
|
||
self.video_container = QWidget()
|
||
self.video_grid = QGridLayout(self.video_container)
|
||
scroll_area.setWidget(self.video_container)
|
||
|
||
return scroll_area
|
||
|
||
def create_info_panel(self):
|
||
"""创建右侧信息面板"""
|
||
panel = QWidget()
|
||
layout = QVBoxLayout(panel)
|
||
|
||
# 系统信息组
|
||
info_group = QGroupBox("系统信息")
|
||
info_group.setStyleSheet("""
|
||
QGroupBox {
|
||
font-weight: bold;
|
||
border: 2px solid #444;
|
||
border-radius: 8px;
|
||
margin-top: 1ex;
|
||
padding-top: 10px;
|
||
}
|
||
QGroupBox::title {
|
||
subcontrol-origin: margin;
|
||
left: 10px;
|
||
padding: 0 5px 0 5px;
|
||
}
|
||
""")
|
||
|
||
info_layout = QVBoxLayout()
|
||
|
||
self.camera_count_label = QLabel("摄像头数量: 0")
|
||
self.frame_count_label = QLabel("处理帧数: 0")
|
||
self.alert_count_label = QLabel("告警数量: 0")
|
||
|
||
info_layout.addWidget(self.camera_count_label)
|
||
info_layout.addWidget(self.frame_count_label)
|
||
info_layout.addWidget(self.alert_count_label)
|
||
info_group.setLayout(info_layout)
|
||
|
||
layout.addWidget(info_group)
|
||
|
||
# 统计图表区域
|
||
stats_group = QGroupBox("告警统计")
|
||
stats_group.setStyleSheet("""
|
||
QGroupBox {
|
||
font-weight: bold;
|
||
border: 2px solid #444;
|
||
border-radius: 8px;
|
||
margin-top: 1ex;
|
||
padding-top: 10px;
|
||
}
|
||
QGroupBox::title {
|
||
subcontrol-origin: margin;
|
||
left: 10px;
|
||
padding: 0 5px 0 5px;
|
||
}
|
||
""")
|
||
|
||
self.stats_label = QLabel("暂无数据")
|
||
self.stats_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
self.stats_label.setStyleSheet("""
|
||
QLabel {
|
||
padding: 20px;
|
||
background-color: #2d2d2d;
|
||
border-radius: 4px;
|
||
color: #888;
|
||
}
|
||
""")
|
||
|
||
stats_layout = QVBoxLayout()
|
||
stats_layout.addWidget(self.stats_label)
|
||
stats_group.setLayout(stats_layout)
|
||
|
||
layout.addWidget(stats_group)
|
||
layout.addStretch()
|
||
|
||
return panel
|
||
|
||
def create_alert_section(self):
|
||
"""创建告警列表区域"""
|
||
layout = QVBoxLayout()
|
||
|
||
# 标题
|
||
title_label = QLabel("🚨 实时告警")
|
||
title_label.setStyleSheet("""
|
||
QLabel {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #FF6B6B;
|
||
margin-bottom: 8px;
|
||
}
|
||
""")
|
||
layout.addWidget(title_label)
|
||
|
||
# 告警列表
|
||
self.alert_list = AlertListWidget()
|
||
layout.addWidget(self.alert_list)
|
||
|
||
return layout
|
||
|
||
def connect_websocket(self):
|
||
"""连接WebSocket服务器"""
|
||
if self.websocket_worker and self.websocket_worker.isRunning():
|
||
return
|
||
|
||
self.statusBar().showMessage("正在连接服务器...")
|
||
|
||
# 创建WebSocket工作线程
|
||
self.websocket_worker = WebSocketWorker("ws://localhost:8765")
|
||
self.websocket_worker.message_received.connect(self.handle_message)
|
||
self.websocket_worker.connection_status.connect(self.handle_connection_status)
|
||
self.websocket_worker.start()
|
||
|
||
def disconnect_websocket(self):
|
||
"""断开WebSocket连接"""
|
||
if self.websocket_worker:
|
||
self.websocket_worker.stop()
|
||
self.websocket_worker.wait()
|
||
self.websocket_worker = None
|
||
|
||
self.status_label.setText("未连接")
|
||
self.status_label.setStyleSheet("""
|
||
QLabel {
|
||
padding: 6px 12px;
|
||
background-color: #666;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
}
|
||
""")
|
||
self.connect_btn.setText("连接服务器")
|
||
self.statusBar().showMessage("已断开连接")
|
||
|
||
def toggle_connection(self):
|
||
"""切换连接状态"""
|
||
if self.websocket_worker and self.websocket_worker.isRunning():
|
||
self.disconnect_websocket()
|
||
else:
|
||
self.connect_websocket()
|
||
|
||
def handle_connection_status(self, connected, message):
|
||
"""处理连接状态变化"""
|
||
if connected:
|
||
self.status_label.setText("已连接")
|
||
self.status_label.setStyleSheet("""
|
||
QLabel {
|
||
padding: 6px 12px;
|
||
background-color: #4CAF50;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
}
|
||
""")
|
||
self.connect_btn.setText("断开连接")
|
||
self.statusBar().showMessage("连接成功")
|
||
else:
|
||
self.status_label.setText("连接失败")
|
||
self.status_label.setStyleSheet("""
|
||
QLabel {
|
||
padding: 6px 12px;
|
||
background-color: #F44336;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
}
|
||
""")
|
||
self.connect_btn.setText("连接服务器")
|
||
self.statusBar().showMessage(message)
|
||
|
||
def handle_message(self, data):
|
||
"""处理接收到的WebSocket消息"""
|
||
msg_type = data.get('msg_type')
|
||
|
||
if msg_type == 'frame':
|
||
self.handle_frame_message(data)
|
||
elif msg_type == 'alert':
|
||
self.handle_alert_message(data)
|
||
|
||
def handle_frame_message(self, data):
|
||
"""处理帧消息"""
|
||
camera_id = data.get('camera_id')
|
||
image_base64 = data.get('image_base64')
|
||
|
||
if not image_base64:
|
||
return
|
||
|
||
# 获取或创建摄像头控件
|
||
if camera_id not in self.camera_widgets:
|
||
self.create_camera_widget(camera_id)
|
||
|
||
# 更新视频显示
|
||
self.camera_widgets[camera_id]['video'].update_image(image_base64)
|
||
|
||
# 更新统计信息
|
||
current_count = int(self.frame_count_label.text().split(': ')[1])
|
||
self.frame_count_label.setText(f"处理帧数: {current_count + 1}")
|
||
|
||
def handle_alert_message(self, data):
|
||
"""处理告警消息"""
|
||
# 添加到告警列表
|
||
self.alert_list.add_alert(data)
|
||
|
||
# 更新统计信息
|
||
current_count = int(self.alert_count_label.text().split(': ')[1])
|
||
self.alert_count_label.setText(f"告警数量: {current_count + 1}")
|
||
|
||
# 状态栏提示
|
||
camera_id = data.get('camera_id')
|
||
event_type = data.get('event_type')
|
||
self.statusBar().showMessage(f"摄像头{camera_id}触发告警 - 事件类型{event_type}")
|
||
|
||
def create_camera_widget(self, camera_id):
|
||
"""创建摄像头视频控件"""
|
||
# 摄像头容器
|
||
camera_widget = QWidget()
|
||
camera_layout = QVBoxLayout(camera_widget)
|
||
|
||
# 标题
|
||
title_label = QLabel(f"摄像头 {camera_id}")
|
||
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
title_label.setStyleSheet("""
|
||
QLabel {
|
||
font-weight: bold;
|
||
font-size: 14px;
|
||
padding: 8px;
|
||
background-color: #0078d4;
|
||
border-radius: 4px 4px 0 0;
|
||
}
|
||
""")
|
||
camera_layout.addWidget(title_label)
|
||
|
||
# 视频显示
|
||
video_widget = VideoWidget()
|
||
camera_layout.addWidget(video_widget)
|
||
|
||
# 状态标签
|
||
status_label = QLabel("🔴 离线")
|
||
status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||
status_label.setStyleSheet("""
|
||
QLabel {
|
||
padding: 4px;
|
||
font-size: 12px;
|
||
background-color: #2d2d2d;
|
||
border-top: 1px solid #444;
|
||
color: #F44336;
|
||
}
|
||
""")
|
||
camera_layout.addWidget(status_label)
|
||
|
||
# 添加到网格布局
|
||
row = (camera_id - 1) // 2
|
||
col = (camera_id - 1) % 2
|
||
self.video_grid.addWidget(camera_widget, row, col)
|
||
|
||
# 保存控件引用
|
||
self.camera_widgets[camera_id] = {
|
||
'video': video_widget,
|
||
'status': status_label
|
||
}
|
||
|
||
# 更新摄像头数量
|
||
self.camera_count_label.setText(f"摄像头数量: {len(self.camera_widgets)}")
|
||
|
||
def refresh_videos(self):
|
||
"""刷新视频显示"""
|
||
# 清空现有视频控件
|
||
for widget_info in self.camera_widgets.values():
|
||
if hasattr(widget_info['video'], 'clear'):
|
||
widget_info['video'].clear()
|
||
|
||
def update_time(self):
|
||
"""更新当前时间显示"""
|
||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
self.time_label.setText(current_time)
|
||
|
||
def show_about(self):
|
||
"""显示关于对话框"""
|
||
QMessageBox.about(self, "关于",
|
||
"AI监控系统 v1.0 (PyQt6)\n\n"
|
||
"基于Python + PyQt6的实时视频监控解决方案\n"
|
||
"支持RTSP视频流接入和AI智能检测\n\n"
|
||
"功能特性:\n"
|
||
"• 多路RTSP视频流监控\n"
|
||
"• 实时AI目标检测\n"
|
||
"• 智能告警推送\n"
|
||
"• 历史视频回放\n"
|
||
"• 现代化图形界面\n\n"
|
||
"技术栈:Python, PyQt6, OpenCV, YOLO, WebSocket")
|
||
|
||
def closeEvent(self, event):
|
||
"""窗口关闭事件"""
|
||
self.disconnect_websocket()
|
||
event.accept()
|
||
|
||
|
||
def main():
|
||
"""主函数"""
|
||
app = QApplication(sys.argv)
|
||
|
||
# 设置应用信息
|
||
app.setApplicationName("AI监控系统")
|
||
app.setApplicationVersion("1.0")
|
||
app.setOrganizationName("AI Monitor")
|
||
|
||
# 创建主窗口
|
||
window = AIMonitorGUI()
|
||
window.show()
|
||
|
||
# 运行应用
|
||
sys.exit(app.exec())
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |