348 lines
14 KiB
Python
348 lines
14 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
主界面窗口
|
||
包含视频显示区域、控制按钮和车牌号显示区域
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
from PyQt5.QtWidgets import (
|
||
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||
QLabel, QPushButton, QFrame, QTextEdit, QGroupBox,
|
||
QCheckBox, QSpinBox, QSlider, QGridLayout
|
||
)
|
||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
|
||
from PyQt5.QtGui import QFont, QPixmap, QPalette, QImage
|
||
|
||
from .video_widget import VideoWidget
|
||
from utils.video_capture import VideoCapture
|
||
from models.yolo_detector import YOLODetector
|
||
from models.plate_recognizer import PlateRecognizerManager
|
||
|
||
class MainWindow(QMainWindow):
|
||
"""主窗口类"""
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.video_capture = None
|
||
self.yolo_detector = None
|
||
self.plate_recognizer = PlateRecognizerManager('mock') # 车牌识别管理器
|
||
self.timer = QTimer()
|
||
self.use_camera = 1 # 1: 摄像头, 0: 视频文件
|
||
self.detected_plates = [] # 存储切割后的车牌图像数组
|
||
self.current_frame = None # 存储当前帧用于车牌矫正
|
||
|
||
self.init_ui()
|
||
self.init_detector()
|
||
self.init_video_capture()
|
||
self.connect_signals()
|
||
|
||
def init_ui(self):
|
||
"""初始化用户界面"""
|
||
self.setWindowTitle("车牌检测系统 - YOLO11s")
|
||
self.setGeometry(100, 100, 1200, 800)
|
||
|
||
# 创建中央widget
|
||
central_widget = QWidget()
|
||
self.setCentralWidget(central_widget)
|
||
|
||
# 主布局
|
||
main_layout = QHBoxLayout(central_widget)
|
||
|
||
# 左侧视频显示区域
|
||
self.create_video_area(main_layout)
|
||
|
||
# 右侧控制和信息显示区域
|
||
self.create_control_area(main_layout)
|
||
|
||
# 设置布局比例
|
||
main_layout.setStretch(0, 3) # 视频区域占3/4
|
||
main_layout.setStretch(1, 1) # 控制区域占1/4
|
||
|
||
def create_video_area(self, parent_layout):
|
||
"""创建视频显示区域"""
|
||
video_frame = QFrame()
|
||
video_frame.setFrameStyle(QFrame.StyledPanel)
|
||
video_layout = QVBoxLayout(video_frame)
|
||
|
||
# 视频显示widget
|
||
self.video_widget = VideoWidget()
|
||
video_layout.addWidget(self.video_widget)
|
||
|
||
parent_layout.addWidget(video_frame)
|
||
|
||
def create_control_area(self, parent_layout):
|
||
"""创建控制和信息显示区域"""
|
||
control_frame = QFrame()
|
||
control_frame.setFrameStyle(QFrame.StyledPanel)
|
||
control_frame.setMaximumWidth(300)
|
||
control_layout = QVBoxLayout(control_frame)
|
||
|
||
# 控制按钮组
|
||
self.create_control_buttons(control_layout)
|
||
|
||
# 检测信息显示
|
||
self.create_detection_info(control_layout)
|
||
|
||
# 车牌号显示区域
|
||
self.create_plate_display(control_layout)
|
||
|
||
# 系统状态显示
|
||
self.create_status_display(control_layout)
|
||
|
||
parent_layout.addWidget(control_frame)
|
||
|
||
def create_control_buttons(self, parent_layout):
|
||
"""创建控制按钮"""
|
||
button_group = QGroupBox("控制面板")
|
||
button_layout = QVBoxLayout(button_group)
|
||
|
||
# 开始/停止按钮
|
||
self.start_btn = QPushButton("开始检测")
|
||
self.start_btn.setMinimumHeight(40)
|
||
self.start_btn.clicked.connect(self.toggle_detection)
|
||
button_layout.addWidget(self.start_btn)
|
||
|
||
# 视频源切换
|
||
self.camera_checkbox = QCheckBox("使用摄像头")
|
||
self.camera_checkbox.setChecked(True)
|
||
self.camera_checkbox.stateChanged.connect(self.toggle_video_source)
|
||
button_layout.addWidget(self.camera_checkbox)
|
||
|
||
# 检测开关
|
||
self.detection_checkbox = QCheckBox("启用检测")
|
||
self.detection_checkbox.setChecked(True)
|
||
button_layout.addWidget(self.detection_checkbox)
|
||
|
||
parent_layout.addWidget(button_group)
|
||
|
||
def create_detection_info(self, parent_layout):
|
||
"""创建检测信息显示"""
|
||
info_group = QGroupBox("检测信息")
|
||
info_layout = QVBoxLayout(info_group)
|
||
|
||
# FPS显示
|
||
self.fps_label = QLabel("FPS: 0")
|
||
self.fps_label.setFont(QFont("Arial", 12, QFont.Bold))
|
||
info_layout.addWidget(self.fps_label)
|
||
|
||
# 检测数量
|
||
self.detection_count_label = QLabel("检测到车牌: 0")
|
||
info_layout.addWidget(self.detection_count_label)
|
||
|
||
# 模型信息
|
||
self.model_info_label = QLabel("模型: YOLO11s (ONNX)")
|
||
info_layout.addWidget(self.model_info_label)
|
||
|
||
parent_layout.addWidget(info_group)
|
||
|
||
def create_plate_display(self, parent_layout):
|
||
"""创建车牌号显示区域"""
|
||
plate_group = QGroupBox("车牌识别结果")
|
||
plate_layout = QVBoxLayout(plate_group)
|
||
|
||
# 当前识别的车牌号
|
||
self.current_plate_label = QLabel("当前车牌: 未识别")
|
||
self.current_plate_label.setFont(QFont("Arial", 14, QFont.Bold))
|
||
self.current_plate_label.setStyleSheet("color: blue; padding: 10px; border: 1px solid gray;")
|
||
plate_layout.addWidget(self.current_plate_label)
|
||
|
||
# 矫正后的车牌图像显示
|
||
self.plate_image_label = QLabel("矫正后车牌图像")
|
||
self.plate_image_label.setAlignment(Qt.AlignCenter)
|
||
self.plate_image_label.setMinimumHeight(100)
|
||
self.plate_image_label.setMaximumHeight(150)
|
||
self.plate_image_label.setStyleSheet("border: 1px solid gray; background-color: #f0f0f0;")
|
||
plate_layout.addWidget(self.plate_image_label)
|
||
|
||
# 历史车牌记录
|
||
history_label = QLabel("历史记录:")
|
||
plate_layout.addWidget(history_label)
|
||
|
||
self.plate_history = QTextEdit()
|
||
self.plate_history.setMaximumHeight(150)
|
||
self.plate_history.setReadOnly(True)
|
||
plate_layout.addWidget(self.plate_history)
|
||
|
||
# 预留接口说明
|
||
interface_label = QLabel("注: 车牌识别接口已预留,可接入OCR模型")
|
||
interface_label.setStyleSheet("color: gray; font-size: 10px;")
|
||
plate_layout.addWidget(interface_label)
|
||
|
||
parent_layout.addWidget(plate_group)
|
||
|
||
def create_status_display(self, parent_layout):
|
||
"""创建系统状态显示"""
|
||
status_group = QGroupBox("系统状态")
|
||
status_layout = QVBoxLayout(status_group)
|
||
|
||
self.status_label = QLabel("状态: 就绪")
|
||
status_layout.addWidget(self.status_label)
|
||
|
||
self.gpu_status_label = QLabel("GPU: 检测中...")
|
||
status_layout.addWidget(self.gpu_status_label)
|
||
|
||
parent_layout.addWidget(status_group)
|
||
|
||
# 添加弹性空间
|
||
parent_layout.addStretch()
|
||
|
||
def init_detector(self):
|
||
"""初始化YOLO检测器"""
|
||
try:
|
||
model_path = os.path.join(os.path.dirname(__file__), "..", "yolo11sth50.onnx")
|
||
self.yolo_detector = YOLODetector(model_path)
|
||
self.model_info_label.setText(f"模型: YOLO11s (ONNX) - GPU: {self.yolo_detector.use_gpu}")
|
||
self.gpu_status_label.setText(f"GPU: {'启用' if self.yolo_detector.use_gpu else '禁用'}")
|
||
except Exception as e:
|
||
self.status_label.setText(f"模型加载失败: {str(e)}")
|
||
|
||
def init_video_capture(self):
|
||
"""初始化视频捕获"""
|
||
try:
|
||
self.video_capture = VideoCapture()
|
||
self.status_label.setText("视频捕获初始化成功")
|
||
except Exception as e:
|
||
self.status_label.setText(f"视频捕获初始化失败: {str(e)}")
|
||
|
||
def connect_signals(self):
|
||
"""连接信号和槽"""
|
||
self.timer.timeout.connect(self.update_frame)
|
||
|
||
def toggle_detection(self):
|
||
"""切换检测状态"""
|
||
if self.timer.isActive():
|
||
self.stop_detection()
|
||
else:
|
||
self.start_detection()
|
||
|
||
def start_detection(self):
|
||
"""开始检测"""
|
||
if self.video_capture and self.video_capture.start_capture(self.use_camera):
|
||
# 根据视频源类型设置定时器间隔
|
||
video_fps = self.video_capture.get_video_fps()
|
||
timer_interval = int(1000 / video_fps) # 转换为毫秒
|
||
self.timer.start(timer_interval)
|
||
|
||
self.start_btn.setText("停止检测")
|
||
source_type = "摄像头" if self.use_camera else f"视频文件({video_fps:.1f}FPS)"
|
||
self.status_label.setText(f"检测中... - {source_type}")
|
||
else:
|
||
self.status_label.setText("启动失败")
|
||
|
||
def stop_detection(self):
|
||
"""停止检测"""
|
||
self.timer.stop()
|
||
if self.video_capture:
|
||
self.video_capture.stop_capture()
|
||
self.start_btn.setText("开始检测")
|
||
self.status_label.setText("已停止")
|
||
|
||
def toggle_video_source(self, state):
|
||
"""切换视频源"""
|
||
self.use_camera = 1 if state == Qt.Checked else 0
|
||
if self.timer.isActive():
|
||
self.stop_detection()
|
||
self.start_detection()
|
||
|
||
def update_frame(self):
|
||
"""更新帧"""
|
||
if not self.video_capture:
|
||
return
|
||
|
||
frame, fps = self.video_capture.get_frame()
|
||
if frame is None:
|
||
return
|
||
|
||
# 保存当前帧用于车牌矫正
|
||
self.current_frame = frame.copy()
|
||
|
||
# 更新FPS显示
|
||
self.fps_label.setText(f"FPS: {fps:.1f}")
|
||
|
||
# 进行检测
|
||
if self.detection_checkbox.isChecked() and self.yolo_detector:
|
||
detections = self.yolo_detector.detect(frame)
|
||
frame = self.yolo_detector.draw_detections(frame, detections)
|
||
|
||
# 切割车牌图像
|
||
if detections:
|
||
self.detected_plates = self.yolo_detector.crop_plates(frame, detections)
|
||
|
||
# 统计不同类型车牌数量
|
||
blue_count = sum(1 for d in detections if d['class_id'] == 0)
|
||
green_count = sum(1 for d in detections if d['class_id'] == 1)
|
||
total_count = len(detections)
|
||
|
||
self.detection_count_label.setText(f"检测到车牌: {total_count} (蓝牌:{blue_count}, 绿牌:{green_count})")
|
||
|
||
# 调用车牌识别接口(预留)
|
||
self.recognize_plates(self.detected_plates, detections)
|
||
else:
|
||
self.detection_count_label.setText("检测到车牌: 0")
|
||
|
||
# 显示帧
|
||
self.video_widget.update_frame(frame)
|
||
|
||
def recognize_plates(self, plate_images, detections):
|
||
"""车牌识别接口(预留)"""
|
||
# 这里是预留的车牌识别接口
|
||
# 可以接入OCR模型进行车牌号识别
|
||
if plate_images and detections and self.current_frame is not None:
|
||
# 获取最新检测到的车牌信息
|
||
latest_detection = detections[-1] # 取最后一个检测结果
|
||
plate_type = "Blue Plate" if latest_detection['class_id'] == 0 else "Green Plate"
|
||
confidence = latest_detection['confidence']
|
||
|
||
# 处理蓝色车牌的矫正
|
||
corrected_image = None
|
||
if latest_detection['class_id'] == 0: # 蓝色车牌
|
||
try:
|
||
bbox = latest_detection['bbox']
|
||
corrected_image = self.plate_recognizer.preprocess_blue_plate(
|
||
plate_images[-1], self.current_frame, bbox
|
||
)
|
||
self._display_plate_image(corrected_image)
|
||
except Exception as e:
|
||
print(f"蓝色车牌矫正失败: {e}")
|
||
self.plate_image_label.setText("蓝色车牌矫正失败")
|
||
elif latest_detection['class_id'] == 1: # 绿色车牌
|
||
# 绿色车牌处理预留
|
||
self.plate_image_label.setText("绿色车牌处理\n(待实现)")
|
||
|
||
# 模拟识别结果
|
||
plate_text = f"Mock {plate_type}-{len(plate_images)}"
|
||
self.current_plate_label.setText(f"Current Plate: {plate_text} (Confidence: {confidence:.2f})")
|
||
|
||
# 添加到历史记录
|
||
import datetime
|
||
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
||
self.plate_history.append(f"[{timestamp}] {plate_text} (Confidence: {confidence:.2f})")
|
||
|
||
def _display_plate_image(self, image):
|
||
"""在界面上显示车牌图像"""
|
||
try:
|
||
# 将OpenCV图像转换为QPixmap
|
||
if len(image.shape) == 3:
|
||
height, width, channel = image.shape
|
||
bytes_per_line = 3 * width
|
||
q_image = QImage(image.data, width, height, bytes_per_line, QImage.Format_RGB888).rgbSwapped()
|
||
else:
|
||
height, width = image.shape
|
||
bytes_per_line = width
|
||
q_image = QImage(image.data, width, height, bytes_per_line, QImage.Format_Grayscale8)
|
||
|
||
# 缩放图像以适应标签大小
|
||
pixmap = QPixmap.fromImage(q_image)
|
||
scaled_pixmap = pixmap.scaled(self.plate_image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||
|
||
self.plate_image_label.setPixmap(scaled_pixmap)
|
||
except Exception as e:
|
||
print(f"显示车牌图像失败: {e}")
|
||
self.plate_image_label.setText(f"图像显示失败: {str(e)}")
|
||
|
||
def closeEvent(self, event):
|
||
"""窗口关闭事件"""
|
||
self.stop_detection()
|
||
event.accept() |