import sys import os import cv2 import numpy as np from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, \ QFileDialog, QFrame, QScrollArea, QComboBox from PyQt5.QtCore import QTimer, Qt, pyqtSignal, QThread from PyQt5.QtGui import QImage, QPixmap, QFont, QPainter, QPen, QColor from yolopart.detector import LicensePlateYOLO #选择使用哪个模块 # from LPRNET_part.lpr_interface import LPRNmodel_predict # from LPRNET_part.lpr_interface import LPRNinitialize_model #使用OCR # from OCR_part.ocr_interface import LPRNmodel_predict # from OCR_part.ocr_interface import LPRNinitialize_model # 使用CRNN # from CRNN_part.crnn_interface import LPRNmodel_predict # from CRNN_part.crnn_interface import LPRNinitialize_model class CameraThread(QThread): """摄像头线程类""" frame_ready = pyqtSignal(np.ndarray) def __init__(self): super().__init__() self.camera = None self.running = False def start_camera(self): """启动摄像头""" self.camera = cv2.VideoCapture(0) if self.camera.isOpened(): self.running = True self.start() return True return False def stop_camera(self): """停止摄像头""" self.running = False if self.camera: self.camera.release() self.quit() self.wait() def run(self): """线程运行函数""" while self.running: if self.camera and self.camera.isOpened(): ret, frame = self.camera.read() if ret: self.frame_ready.emit(frame) self.msleep(30) # 约30fps class VideoThread(QThread): """视频处理线程类""" frame_ready = pyqtSignal(np.ndarray) video_finished = pyqtSignal() def __init__(self): super().__init__() self.video_path = None self.cap = None self.running = False self.paused = False def load_video(self, video_path): """加载视频文件""" self.video_path = video_path self.cap = cv2.VideoCapture(video_path) return self.cap.isOpened() def start_video(self): """开始播放视频""" if self.cap and self.cap.isOpened(): self.running = True self.paused = False self.start() return True return False def pause_video(self): """暂停/继续视频""" self.paused = not self.paused return self.paused def stop_video(self): """停止视频""" self.running = False if self.cap: self.cap.release() self.quit() self.wait() def run(self): """线程运行函数""" while self.running: if not self.paused and self.cap and self.cap.isOpened(): ret, frame = self.cap.read() if ret: self.frame_ready.emit(frame) else: # 视频播放结束 self.video_finished.emit() self.running = False break self.msleep(30) # 约30fps class LicensePlateWidget(QWidget): """单个车牌结果显示组件""" def __init__(self, plate_id, class_name, corrected_image, plate_number): super().__init__() self.plate_id = plate_id self.init_ui(class_name, corrected_image, plate_number) def init_ui(self, class_name, corrected_image, plate_number): layout = QHBoxLayout() layout.setContentsMargins(10, 5, 10, 5) # 车牌类型标签 type_label = QLabel(class_name) type_label.setFixedWidth(60) type_label.setAlignment(Qt.AlignCenter) type_label.setStyleSheet( "QLabel { " "background-color: #4CAF50 if class_name == '绿牌' else #2196F3; " "color: white; " "border-radius: 5px; " "padding: 5px; " "font-weight: bold; " "}" ) if class_name == '绿牌': type_label.setStyleSheet( "QLabel { " "background-color: #4CAF50; " "color: white; " "border-radius: 5px; " "padding: 5px; " "font-weight: bold; " "}" ) else: type_label.setStyleSheet( "QLabel { " "background-color: #2196F3; " "color: white; " "border-radius: 5px; " "padding: 5px; " "font-weight: bold; " "}" ) # 矫正后的车牌图像 image_label = QLabel() image_label.setFixedSize(120, 40) image_label.setStyleSheet("border: 1px solid #ddd; background-color: white;") if corrected_image is not None: # 转换numpy数组为QPixmap h, w = corrected_image.shape[:2] if len(corrected_image.shape) == 3: bytes_per_line = 3 * w q_image = QImage(corrected_image.data, w, h, bytes_per_line, QImage.Format_RGB888).rgbSwapped() else: bytes_per_line = w q_image = QImage(corrected_image.data, w, h, bytes_per_line, QImage.Format_Grayscale8) pixmap = QPixmap.fromImage(q_image) scaled_pixmap = pixmap.scaled(120, 40, Qt.KeepAspectRatio, Qt.SmoothTransformation) image_label.setPixmap(scaled_pixmap) else: image_label.setText("车牌未完全\n进入摄像头") image_label.setAlignment(Qt.AlignCenter) image_label.setStyleSheet("border: 1px solid #ddd; background-color: #f5f5f5; color: #666;") # 车牌号标签 number_label = QLabel(plate_number) number_label.setFixedWidth(150) number_label.setAlignment(Qt.AlignCenter) number_label.setStyleSheet( "QLabel { " "border: 1px solid #ddd; " "background-color: white; " "padding: 8px; " "font-family: 'Courier New'; " "font-size: 14px; " "font-weight: bold; " "}" ) layout.addWidget(type_label) layout.addWidget(image_label) layout.addWidget(number_label) layout.addStretch() self.setLayout(layout) self.setStyleSheet( "QWidget { " "background-color: white; " "border: 1px solid #e0e0e0; " "border-radius: 8px; " "margin: 2px; " "}" ) class MainWindow(QMainWindow): """主窗口类""" def __init__(self): super().__init__() self.detector = None self.camera_thread = None self.video_thread = None self.current_frame = None self.detections = [] self.current_mode = "camera" # 当前模式:camera, video, image self.is_processing = False # 标志位,表示是否正在处理识别任务 self.last_plate_results = [] # 存储上一次的车牌识别结果 self.current_recognition_method = "CRNN" # 当前识别方法 self.init_ui() self.init_detector() self.init_camera() self.init_video() # 初始化默认识别方法(CRNN)的模型 self.change_recognition_method(self.current_recognition_method) def init_ui(self): """初始化用户界面""" self.setWindowTitle("车牌识别系统") self.setGeometry(100, 100, 1200, 800) # 创建中央widget central_widget = QWidget() self.setCentralWidget(central_widget) # 创建主布局 main_layout = QHBoxLayout(central_widget) # 左侧摄像头显示区域 left_frame = QFrame() left_frame.setFrameStyle(QFrame.StyledPanel) left_frame.setStyleSheet("QFrame { background-color: #f0f0f0; border: 2px solid #ddd; }") left_layout = QVBoxLayout(left_frame) # 摄像头显示标签 self.camera_label = QLabel() self.camera_label.setMinimumSize(640, 480) self.camera_label.setStyleSheet("QLabel { background-color: black; border: 1px solid #ccc; }") self.camera_label.setAlignment(Qt.AlignCenter) self.camera_label.setText("摄像头未启动") self.camera_label.setScaledContents(False) # 控制按钮 button_layout = QHBoxLayout() self.start_button = QPushButton("启动摄像头") self.stop_button = QPushButton("停止摄像头") self.start_button.clicked.connect(self.start_camera) self.stop_button.clicked.connect(self.stop_camera) self.stop_button.setEnabled(False) # 视频控制按钮 self.open_video_button = QPushButton("打开视频") self.stop_video_button = QPushButton("停止视频") self.pause_video_button = QPushButton("暂停视频") self.open_video_button.clicked.connect(self.open_video_file) self.stop_video_button.clicked.connect(self.stop_video) self.pause_video_button.clicked.connect(self.pause_video) self.stop_video_button.setEnabled(False) self.pause_video_button.setEnabled(False) # 图片控制按钮 self.open_image_button = QPushButton("打开图片") self.open_image_button.clicked.connect(self.open_image_file) button_layout.addWidget(self.start_button) button_layout.addWidget(self.stop_button) button_layout.addWidget(self.open_video_button) button_layout.addWidget(self.stop_video_button) button_layout.addWidget(self.pause_video_button) button_layout.addWidget(self.open_image_button) button_layout.addStretch() left_layout.addWidget(self.camera_label) left_layout.addLayout(button_layout) # 右侧结果显示区域 right_frame = QFrame() right_frame.setFrameStyle(QFrame.StyledPanel) right_frame.setFixedWidth(400) right_frame.setStyleSheet("QFrame { background-color: #fafafa; border: 2px solid #ddd; }") right_layout = QVBoxLayout(right_frame) # 标题 title_label = QLabel("检测结果") title_label.setAlignment(Qt.AlignCenter) title_label.setFont(QFont("Arial", 16, QFont.Bold)) title_label.setStyleSheet("QLabel { color: #333; padding: 10px; }") # 识别方法选择 method_layout = QHBoxLayout() method_label = QLabel("识别方法:") method_label.setFont(QFont("Arial", 10)) self.method_combo = QComboBox() self.method_combo.addItems(["CRNN", "LPRNET", "OCR"]) self.method_combo.setCurrentText("CRNN") # 默认选择CRNN self.method_combo.currentTextChanged.connect(self.change_recognition_method) method_layout.addWidget(method_label) method_layout.addWidget(self.method_combo) method_layout.addStretch() # 车牌数量显示 self.count_label = QLabel("识别到的车牌数量: 0") self.count_label.setAlignment(Qt.AlignCenter) self.count_label.setFont(QFont("Arial", 12)) self.count_label.setStyleSheet( "QLabel { " "background-color: #e3f2fd; " "border: 1px solid #2196f3; " "border-radius: 5px; " "padding: 8px; " "color: #1976d2; " "font-weight: bold; " "}" ) # 滚动区域用于显示车牌结果 scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setStyleSheet("QScrollArea { border: none; background-color: transparent; }") self.results_widget = QWidget() self.results_layout = QVBoxLayout(self.results_widget) self.results_layout.setAlignment(Qt.AlignTop) scroll_area.setWidget(self.results_widget) # 当前识别任务显示 self.current_method_label = QLabel("当前识别方法: CRNN") self.current_method_label.setAlignment(Qt.AlignRight) self.current_method_label.setFont(QFont("Arial", 9)) self.current_method_label.setStyleSheet("QLabel { color: #666; padding: 5px; }") right_layout.addWidget(title_label) right_layout.addLayout(method_layout) right_layout.addWidget(self.count_label) right_layout.addWidget(scroll_area) right_layout.addWidget(self.current_method_label) # 添加到主布局 main_layout.addWidget(left_frame, 2) main_layout.addWidget(right_frame, 1) # 设置样式 self.setStyleSheet(""" QMainWindow { background-color: #f5f5f5; } QPushButton { background-color: #2196F3; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #1976D2; } QPushButton:pressed { background-color: #0D47A1; } QPushButton:disabled { background-color: #cccccc; color: #666666; } """) def init_detector(self): """初始化检测器""" model_path = os.path.join(os.path.dirname(__file__), "yolopart", "yolo11s-pose42.pt") self.detector = LicensePlateYOLO(model_path) def init_camera(self): """初始化摄像头线程""" self.camera_thread = CameraThread() self.camera_thread.frame_ready.connect(self.process_frame) def init_video(self): """初始化视频线程""" self.video_thread = VideoThread() self.video_thread.frame_ready.connect(self.process_frame) self.video_thread.video_finished.connect(self.on_video_finished) def start_camera(self): """启动摄像头""" if self.camera_thread.start_camera(): self.start_button.setEnabled(False) self.stop_button.setEnabled(True) self.camera_label.setText("摄像头启动中...") else: self.camera_label.setText("摄像头启动失败") def stop_camera(self): """停止摄像头""" self.camera_thread.stop_camera() self.start_button.setEnabled(True) self.stop_button.setEnabled(False) self.camera_label.setText("摄像头已停止") # 只在摄像头模式下清除标签内容 if self.current_mode == "camera": self.camera_label.clear() def on_video_finished(self): """视频播放结束时的处理""" self.video_thread.stop_video() self.open_video_button.setEnabled(True) self.stop_video_button.setEnabled(False) self.pause_video_button.setEnabled(False) self.camera_label.setText("视频播放结束") self.current_mode = "camera" def open_video_file(self): """打开视频文件""" # 停止当前模式 if self.current_mode == "camera" and self.camera_thread and self.camera_thread.running: self.stop_camera() elif self.current_mode == "video" and self.video_thread and self.video_thread.running: self.stop_video() # 选择视频文件 video_path, _ = QFileDialog.getOpenFileName(self, "选择视频文件", "", "视频文件 (*.mp4 *.avi *.mov *.mkv)") if video_path: if self.video_thread.load_video(video_path): self.current_mode = "video" self.start_video() self.camera_label.setText(f"正在播放视频: {os.path.basename(video_path)}") else: self.camera_label.setText("视频加载失败") def start_video(self): """开始播放视频""" if self.video_thread.start_video(): self.open_video_button.setEnabled(False) self.stop_video_button.setEnabled(True) self.pause_video_button.setEnabled(True) self.pause_video_button.setText("暂停") else: self.camera_label.setText("视频播放失败") def pause_video(self): """暂停/继续视频""" if self.video_thread.pause_video(): self.pause_video_button.setText("继续") else: self.pause_video_button.setText("暂停") def stop_video(self): """停止视频""" self.video_thread.stop_video() self.open_video_button.setEnabled(True) self.stop_video_button.setEnabled(False) self.pause_video_button.setEnabled(False) self.camera_label.setText("视频已停止") # 只在视频模式下清除标签内容 if self.current_mode == "video": self.camera_label.clear() self.current_mode = "camera" def open_image_file(self): """打开图片文件""" # 停止当前模式 if self.current_mode == "camera" and self.camera_thread and self.camera_thread.running: self.stop_camera() elif self.current_mode == "video" and self.video_thread and self.video_thread.running: self.stop_video() # 选择图片文件 image_path, _ = QFileDialog.getOpenFileName(self, "选择图片文件", "", "图片文件 (*.jpg *.jpeg *.png *.bmp)") if image_path: self.current_mode = "image" try: # 读取图片 - 方法1: 使用cv2.imdecode处理中文路径 image = cv2.imdecode(np.fromfile(image_path, dtype=np.uint8), cv2.IMREAD_COLOR) # 如果方法1失败,尝试方法2: 直接使用cv2.imread if image is None: image = cv2.imread(image_path) if image is not None: print(f"成功加载图片: {image_path}, 尺寸: {image.shape}") self.process_image(image) # 不在这里设置文本,避免覆盖图片 # self.camera_label.setText(f"正在显示图片: {os.path.basename(image_path)}") else: print(f"图片加载失败: {image_path}") self.camera_label.setText("图片加载失败") except Exception as e: print(f"图片处理异常: {str(e)}") self.camera_label.setText(f"图片处理错误: {str(e)}") def process_image(self, image): """处理图片""" try: print(f"开始处理图片,图片尺寸: {image.shape}") self.current_frame = image.copy() # 进行车牌检测 print("正在进行车牌检测...") self.detections = self.detector.detect_license_plates(image) print(f"检测到 {len(self.detections)} 个车牌") # 在图像上绘制检测结果 print("正在绘制检测结果...") display_frame = self.draw_detections(image.copy()) # 转换为Qt格式并显示 print("正在显示图片...") self.display_frame(display_frame) # 更新右侧结果显示 print("正在更新结果显示...") self.update_results_display() print("图片处理完成") except Exception as e: print(f"图片处理过程中出错: {str(e)}") import traceback traceback.print_exc() def process_frame(self, frame): """处理摄像头帧""" self.current_frame = frame.copy() # 先显示原始帧,保证视频流畅播放 self.display_frame(frame) # 如果当前没有在处理识别任务,则开始新的识别任务 if not self.is_processing: self.is_processing = True # 异步进行车牌检测和识别 QTimer.singleShot(0, self.async_detect_and_update) def async_detect_and_update(self): """异步进行车牌检测和识别""" if self.current_frame is None: self.is_processing = False # 重置标志位 return try: # 进行车牌检测 self.detections = self.detector.detect_license_plates(self.current_frame) # 在图像上绘制检测结果 display_frame = self.draw_detections(self.current_frame.copy()) # 更新显示帧(显示带检测结果的帧) # 无论是摄像头模式还是视频模式,都显示检测框 self.display_frame(display_frame) # 更新右侧结果显示 self.update_results_display() except Exception as e: print(f"异步检测和更新失败: {str(e)}") import traceback traceback.print_exc() finally: # 无论成功或失败,都要重置标志位 self.is_processing = False def draw_detections(self, frame): """在图像上绘制检测结果""" return self.detector.draw_detections(frame, self.detections) def display_frame(self, frame): """显示帧到界面""" try: print(f"开始显示帧,帧尺寸: {frame.shape}") # 方法1: 标准方法 try: rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb_frame.shape bytes_per_line = ch * w qt_image = QImage(rgb_frame.data, w, h, bytes_per_line, QImage.Format_RGB888) print(f"方法1: 创建QImage,尺寸: {qt_image.width()}x{qt_image.height()}") if qt_image.isNull(): print("方法1: QImage为空,尝试方法2") raise Exception("QImage为空") pixmap = QPixmap.fromImage(qt_image) if pixmap.isNull(): print("方法1: QPixmap为空,尝试方法2") raise Exception("QPixmap为空") # 手动缩放图片以适应标签大小,保持宽高比 scaled_pixmap = pixmap.scaled(self.camera_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) self.camera_label.setPixmap(scaled_pixmap) print("方法1: 帧显示完成") return except Exception as e1: print(f"方法1失败: {str(e1)}") # 方法2: 使用imencode和imdecode try: print("尝试方法2: 使用imencode和imdecode") _, buffer = cv2.imencode('.jpg', frame) rgb_frame = cv2.imdecode(buffer, cv2.IMREAD_COLOR) rgb_frame = cv2.cvtColor(rgb_frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb_frame.shape bytes_per_line = ch * w qt_image = QImage(rgb_frame.data, w, h, bytes_per_line, QImage.Format_RGB888) print(f"方法2: 创建QImage,尺寸: {qt_image.width()}x{qt_image.height()}") if qt_image.isNull(): print("方法2: QImage为空") raise Exception("QImage为空") pixmap = QPixmap.fromImage(qt_image) if pixmap.isNull(): print("方法2: QPixmap为空") raise Exception("QPixmap为空") # 手动缩放图片以适应标签大小,保持宽高比 scaled_pixmap = pixmap.scaled(self.camera_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) self.camera_label.setPixmap(scaled_pixmap) print("方法2: 帧显示完成") return except Exception as e2: print(f"方法2失败: {str(e2)}") # 方法3: 直接使用QImage的构造函数 try: print("尝试方法3: 直接使用QImage的构造函数") height, width, channel = frame.shape bytes_per_line = 3 * width q_image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_BGR888) print(f"方法3: 创建QImage,尺寸: {q_image.width()}x{q_image.height()}") if q_image.isNull(): print("方法3: QImage为空") raise Exception("QImage为空") pixmap = QPixmap.fromImage(q_image) if pixmap.isNull(): print("方法3: QPixmap为空") raise Exception("QPixmap为空") # 手动缩放图片以适应标签大小,保持宽高比 scaled_pixmap = pixmap.scaled(self.camera_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) self.camera_label.setPixmap(scaled_pixmap) print("方法3: 帧显示完成") return except Exception as e3: print(f"方法3失败: {str(e3)}") # 所有方法都失败 print("所有显示方法都失败") self.camera_label.setText("图片显示失败") except Exception as e: print(f"显示帧过程中出错: {str(e)}") import traceback traceback.print_exc() self.camera_label.setText(f"显示错误: {str(e)}") def update_results_display(self): """更新右侧结果显示""" # 更新车牌数量 count = len(self.detections) self.count_label.setText(f"识别到的车牌数量: {count}") # 准备新的车牌结果列表 new_plate_results = [] for i, detection in enumerate(self.detections): # 矫正车牌图像 corrected_image = self.correct_license_plate(detection) # 获取车牌号,传入车牌类型信息 plate_number = self.recognize_plate_number(corrected_image, detection['class_name']) # 添加到新结果列表 new_plate_results.append({ 'id': i + 1, 'class_name': detection['class_name'], 'corrected_image': corrected_image, 'plate_number': plate_number }) # 比较新旧结果是否相同 results_changed = False if len(self.last_plate_results) != len(new_plate_results): results_changed = True else: for i in range(len(new_plate_results)): if i >= len(self.last_plate_results): results_changed = True break last_result = self.last_plate_results[i] new_result = new_plate_results[i] # 比较车牌类型和车牌号 if (last_result['class_name'] != new_result['class_name'] or last_result['plate_number'] != new_result['plate_number']): results_changed = True break # 只有当结果发生变化时才更新显示 if results_changed: # 清除之前的结果 for i in reversed(range(self.results_layout.count())): child = self.results_layout.itemAt(i).widget() if child: child.setParent(None) # 添加新的结果 for result in new_plate_results: plate_widget = LicensePlateWidget( result['id'], result['class_name'], result['corrected_image'], result['plate_number'] ) self.results_layout.addWidget(plate_widget) # 更新存储的上一次结果 self.last_plate_results = new_plate_results def correct_license_plate(self, detection): """矫正车牌图像""" if self.current_frame is None: return None # 检查是否为不完整检测 if detection.get('incomplete', False): return None # 使用检测器的矫正方法 return self.detector.correct_license_plate( self.current_frame, detection['keypoints'] ) def recognize_plate_number(self, corrected_image, class_name): """识别车牌号""" if corrected_image is None: return "识别失败" try: # 根据当前选择的识别方法调用相应的函数 if self.current_recognition_method == "CRNN": from CRNN_part.crnn_interface import LPRNmodel_predict elif self.current_recognition_method == "LPRNET": from LPRNET_part.lpr_interface import LPRNmodel_predict elif self.current_recognition_method == "OCR": from OCR_part.ocr_interface import LPRNmodel_predict # 预测函数(来自模块) result = LPRNmodel_predict(corrected_image) # 将字符列表转换为字符串,支持8位车牌号 if isinstance(result, list) and len(result) >= 7: # 根据车牌类型决定显示位数 if class_name == '绿牌' and len(result) >= 8: # 绿牌显示8位,过滤掉空字符占位符 plate_chars = [char for char in result[:8] if char != ''] # 如果过滤后确实有8位,显示8位;否则显示7位 if len(plate_chars) == 8: return ''.join(plate_chars) else: return ''.join(plate_chars[:7]) else: # 蓝牌或其他类型显示前7位,过滤掉空字符 plate_chars = [char for char in result[:7] if char != ''] return ''.join(plate_chars) else: return "识别失败" except Exception as e: print(f"车牌号识别失败: {e}") return "识别失败" def change_recognition_method(self, method): """切换识别方法""" self.current_recognition_method = method self.current_method_label.setText(f"当前识别方法: {method}") # 初始化对应的模型 if method == "CRNN": from CRNN_part.crnn_interface import LPRNinitialize_model LPRNinitialize_model() elif method == "LPRNET": from LPRNET_part.lpr_interface import LPRNinitialize_model LPRNinitialize_model() elif method == "OCR": from OCR_part.ocr_interface import LPRNinitialize_model LPRNinitialize_model() # 如果当前有显示的帧,重新处理以更新识别结果 if self.current_frame is not None: self.process_frame(self.current_frame) def closeEvent(self, event): """窗口关闭事件""" if self.camera_thread and self.camera_thread.running: self.camera_thread.stop_camera() if self.video_thread and self.video_thread.running: self.video_thread.stop_video() event.accept() def main(): app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_()) if __name__ == "__main__": main()