diff --git a/.idea/License_plate_recognition.iml b/.idea/License_plate_recognition.iml index fb56de3..8169cd0 100644 --- a/.idea/License_plate_recognition.iml +++ b/.idea/License_plate_recognition.iml @@ -5,8 +5,4 @@ - - \ No newline at end of file diff --git a/LPRNET_part/1.jpg b/LPRNET_part/1.jpg new file mode 100644 index 0000000..32ddef3 Binary files /dev/null and b/LPRNET_part/1.jpg differ diff --git a/LPRNET_part/2.jpg b/LPRNET_part/2.jpg new file mode 100644 index 0000000..29d94e6 Binary files /dev/null and b/LPRNET_part/2.jpg differ diff --git a/LPRNET_part/6ce2ec7dbed6cf3c8403abe2683c57e5.jpg b/LPRNET_part/6ce2ec7dbed6cf3c8403abe2683c57e5.jpg new file mode 100644 index 0000000..3f8a8f8 Binary files /dev/null and b/LPRNET_part/6ce2ec7dbed6cf3c8403abe2683c57e5.jpg differ diff --git a/LPRNET_part/LPRNet__iteration_74000.pth b/LPRNET_part/LPRNet__iteration_74000.pth new file mode 100644 index 0000000..037122c Binary files /dev/null and b/LPRNET_part/LPRNet__iteration_74000.pth differ diff --git a/LPRNET_part/c11304d10bcd47911e458398d1ea445d.jpg b/LPRNET_part/c11304d10bcd47911e458398d1ea445d.jpg new file mode 100644 index 0000000..570ae94 Binary files /dev/null and b/LPRNET_part/c11304d10bcd47911e458398d1ea445d.jpg differ diff --git a/LPRNET_part/c6ab0fbcfb2b6fbe15c5b3eb9806a28b.jpg b/LPRNET_part/c6ab0fbcfb2b6fbe15c5b3eb9806a28b.jpg new file mode 100644 index 0000000..843a03d Binary files /dev/null and b/LPRNET_part/c6ab0fbcfb2b6fbe15c5b3eb9806a28b.jpg differ diff --git a/LPRNET_part/lpr_interface.py b/LPRNET_part/lpr_interface.py index 2b688ba..0f87201 100644 --- a/LPRNET_part/lpr_interface.py +++ b/LPRNET_part/lpr_interface.py @@ -1,3 +1,4 @@ +# 导入必要的库 import torch import torch.nn as nn import cv2 @@ -11,6 +12,7 @@ from PIL import Image sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # LPRNet字符集定义(与训练时保持一致) +# 包含中国省份简称、数字、字母和特殊字符 CHARS = ['京', '沪', '津', '渝', '冀', '晋', '蒙', '辽', '吉', '黑', '苏', '浙', '皖', '闽', '赣', '鲁', '豫', '鄂', '湘', '粤', '桂', '琼', '川', '贵', '云', '藏', '陕', '甘', '青', '宁', '新', @@ -19,84 +21,115 @@ CHARS = ['京', '沪', '津', '渝', '冀', '晋', '蒙', '辽', '吉', '黑', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'I', 'O', '-'] +# 创建字符到索引的映射字典 CHARS_DICT = {char: i for i, char in enumerate(CHARS)} -# 简化的LPRNet模型定义 +# 简化的LPRNet模型定义 - 基础卷积块 class small_basic_block(nn.Module): def __init__(self, ch_in, ch_out): super(small_basic_block, self).__init__() + # 定义一个小的基本卷积块,包含四个卷积层 self.block = nn.Sequential( + # 1x1卷积,降低通道数 nn.Conv2d(ch_in, ch_out // 4, kernel_size=1), nn.ReLU(), + # 3x1卷积,处理水平特征 nn.Conv2d(ch_out // 4, ch_out // 4, kernel_size=(3, 1), padding=(1, 0)), nn.ReLU(), + # 1x3卷积,处理垂直特征 nn.Conv2d(ch_out // 4, ch_out // 4, kernel_size=(1, 3), padding=(0, 1)), nn.ReLU(), + # 1x1卷积,恢复通道数 nn.Conv2d(ch_out // 4, ch_out, kernel_size=1), ) def forward(self, x): return self.block(x) +# LPRNet模型定义 - 车牌识别网络 class LPRNet(nn.Module): def __init__(self, lpr_max_len, phase, class_num, dropout_rate): super(LPRNet, self).__init__() self.phase = phase self.lpr_max_len = lpr_max_len self.class_num = class_num + + # 定义主干网络 self.backbone = nn.Sequential( + # 初始卷积层 nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, stride=1), # 0 nn.BatchNorm2d(num_features=64), nn.ReLU(), # 2 + # 最大池化层 nn.MaxPool3d(kernel_size=(1, 3, 3), stride=(1, 1, 1)), + # 第一个基本块 small_basic_block(ch_in=64, ch_out=128), # *** 4 *** nn.BatchNorm2d(num_features=128), nn.ReLU(), # 6 + # 第二个池化层 nn.MaxPool3d(kernel_size=(1, 3, 3), stride=(2, 1, 2)), + # 第二个基本块 small_basic_block(ch_in=64, ch_out=256), # 8 nn.BatchNorm2d(num_features=256), nn.ReLU(), # 10 + # 第三个基本块 small_basic_block(ch_in=256, ch_out=256), # *** 11 *** nn.BatchNorm2d(num_features=256), nn.ReLU(), # 13 + # 第三个池化层 nn.MaxPool3d(kernel_size=(1, 3, 3), stride=(4, 1, 2)), # 14 + # Dropout层,防止过拟合 nn.Dropout(dropout_rate), + # 特征提取卷积层 nn.Conv2d(in_channels=64, out_channels=256, kernel_size=(1, 4), stride=1), # 16 nn.BatchNorm2d(num_features=256), nn.ReLU(), # 18 + # 第二个Dropout层 nn.Dropout(dropout_rate), + # 分类卷积层 nn.Conv2d(in_channels=256, out_channels=class_num, kernel_size=(13, 1), stride=1), # 20 nn.BatchNorm2d(num_features=class_num), nn.ReLU(), # 22 ) + + # 定义容器层,用于融合全局上下文信息 self.container = nn.Sequential( nn.Conv2d(in_channels=448+self.class_num, out_channels=self.class_num, kernel_size=(1,1), stride=(1,1)), ) def forward(self, x): + # 保存中间特征 keep_features = list() for i, layer in enumerate(self.backbone.children()): x = layer(x) + # 保存特定层的输出特征 if i in [2, 6, 13, 22]: # [2, 4, 8, 11, 22] keep_features.append(x) + # 处理全局上下文信息 global_context = list() for i, f in enumerate(keep_features): + # 对不同层的特征进行不同尺度的平均池化 if i in [0, 1]: f = nn.AvgPool2d(kernel_size=5, stride=5)(f) if i in [2]: f = nn.AvgPool2d(kernel_size=(4, 10), stride=(4, 2))(f) + # 对特征进行归一化处理 f_pow = torch.pow(f, 2) f_mean = torch.mean(f_pow) f = torch.div(f, f_mean) global_context.append(f) + # 拼接全局上下文特征 x = torch.cat(global_context, 1) + # 通过容器层处理 x = self.container(x) + # 对序列维度进行平均,得到最终输出 logits = torch.mean(x, dim=2) return logits +# LPRNet推理类 class LPRNetInference: def __init__(self, model_path=None, img_size=[94, 24], lpr_max_len=8, dropout_rate=0.5): """ @@ -109,6 +142,7 @@ class LPRNetInference: """ self.img_size = img_size self.lpr_max_len = lpr_max_len + # 检测是否有可用的CUDA设备 self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 设置默认模型路径 @@ -130,6 +164,7 @@ class LPRNetInference: else: print(f"Warning: 模型文件不存在或未指定: {model_path}. 使用随机权重.") + # 将模型移动到指定设备并设置为评估模式 self.model.to(self.device) self.model.eval() @@ -164,9 +199,11 @@ class LPRNetInference: image_array = cv2.resize(image_array, tuple(self.img_size)) # 使用与训练时相同的预处理方式 + # 归一化处理:减去127.5并乘以0.0078125,将像素值从[0,255]映射到[-1,1] image_array = image_array.astype('float32') image_array -= 127.5 image_array *= 0.0078125 + # 调整维度顺序从HWC到CHW image_array = np.transpose(image_array, (2, 0, 1)) # HWC -> CHW # 转换为tensor并添加batch维度 @@ -186,7 +223,7 @@ class LPRNetInference: prebs = logits.cpu().detach().numpy() preb = prebs[0, :, :] # 取第一个batch [num_classes, sequence_length] - # 贪婪解码:对每个时间步选择最大概率的字符 + # 贪婪解码: 对每个时间步选择最大概率的字符 preb_label = [] for j in range(preb.shape[1]): # 遍历每个时间步 preb_label.append(np.argmax(preb[:, j], axis=0)) @@ -248,7 +285,7 @@ class LPRNetInference: print(f"预测图像失败: {e}") return None, 0.0 -# 全局变量 +# 全局变量,用于存储模型实例 lpr_model = None def LPRNinitialize_model(): @@ -295,6 +332,13 @@ def LPRNmodel_predict(image_array): return ['待', '识', '别', '0', '0', '0', '0', '0'] try: + # 使用OpenCV调整图像大小到模型要求的尺寸 + image_array = cv2.resize(image_array, (94, 24)) + print(f"666999图片尺寸: {image_array.shape}") + + # 显示修正后的图像 + cv2.imshow('Resized License Plate Image (94x24)', image_array) + cv2.waitKey(1) # 非阻塞显示,允许程序继续执行 # 预测车牌号 predicted_text, confidence = lpr_model.predict(image_array) diff --git a/OCR_part/ocr_interface.py b/OCR_part/ocr_interface.py index b98c5b8..75770c0 100644 --- a/OCR_part/ocr_interface.py +++ b/OCR_part/ocr_interface.py @@ -5,6 +5,18 @@ import cv2 class OCRProcessor: def __init__(self): self.model = TextRecognition(model_name="PP-OCRv5_server_rec") + # 定义允许的字符集合(不包含空白字符) + self.allowed_chars = [ + # 中文省份简称 + '京', '沪', '津', '渝', '冀', '晋', '蒙', '辽', '吉', '黑', + '苏', '浙', '皖', '闽', '赣', '鲁', '豫', '鄂', '湘', '粤', + '桂', '琼', '川', '贵', '云', '藏', '陕', '甘', '青', '宁', '新', + # 字母 A-Z + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + # 数字 0-9 + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' + ] print("OCR模型初始化完成(占位)") def predict(self, image_array): @@ -14,6 +26,14 @@ class OCRProcessor: results = output[0]["rec_text"] placeholder_result = results.split(',') return placeholder_result + + def filter_allowed_chars(self, text): + """只保留允许的字符""" + filtered_text = "" + for char in text: + if char in self.allowed_chars: + filtered_text += char + return filtered_text # 保留原有函数接口 _processor = OCRProcessor() @@ -42,8 +62,12 @@ def LPRNmodel_predict(image_array): else: result_str = str(raw_result) - # 过滤掉'·'字符 + # 过滤掉'·'和'-'字符 filtered_str = result_str.replace('·', '') + filtered_str = filtered_str.replace('-', '') + + # 只保留允许的字符 + filtered_str = _processor.filter_allowed_chars(filtered_str) # 转换为字符列表 char_list = list(filtered_str) diff --git a/lightCRNN_part/best_model.pth b/lightCRNN_part/best_model.pth new file mode 100644 index 0000000..122cf25 Binary files /dev/null and b/lightCRNN_part/best_model.pth differ diff --git a/main.py b/main.py index e0d6477..132ad5a 100644 --- a/main.py +++ b/main.py @@ -1,25 +1,22 @@ import sys +import os import cv2 import numpy as np -from PyQt5.QtWidgets import ( - QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, - QLabel, QPushButton, QScrollArea, QFrame, QSizePolicy -) +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 -import os from yolopart.detector import LicensePlateYOLO #选择使用哪个模块 -from LPRNET_part.lpr_interface import LPRNmodel_predict -from LPRNET_part.lpr_interface import LPRNinitialize_model - +# 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 +# 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 +# from CRNN_part.crnn_interface import LPRNmodel_predict +# from CRNN_part.crnn_interface import LPRNinitialize_model class CameraThread(QThread): """摄像头线程类""" @@ -56,6 +53,60 @@ class CameraThread(QThread): 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): """单个车牌结果显示组件""" @@ -162,15 +213,21 @@ class MainWindow(QMainWindow): 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() - # 初始化OCR/CRNN模型(函数名改成一样的了,所以不要修改这里了,想用哪个模块直接导入) - LPRNinitialize_model() + # 初始化默认识别方法(CRNN)的模型 + self.change_recognition_method(self.current_recognition_method) def init_ui(self): @@ -197,7 +254,7 @@ class MainWindow(QMainWindow): 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(True) + self.camera_label.setScaledContents(False) # 控制按钮 button_layout = QHBoxLayout() @@ -207,8 +264,26 @@ class MainWindow(QMainWindow): 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) @@ -227,6 +302,20 @@ class MainWindow(QMainWindow): 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) @@ -253,9 +342,17 @@ class MainWindow(QMainWindow): 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) @@ -296,6 +393,12 @@ class MainWindow(QMainWindow): 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(): @@ -311,23 +414,167 @@ class MainWindow(QMainWindow): self.start_button.setEnabled(True) self.stop_button.setEnabled(False) self.camera_label.setText("摄像头已停止") - self.camera_label.clear() + # 只在摄像头模式下清除标签内容 + 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.detections = self.detector.detect_license_plates(frame) + # 先显示原始帧,保证视频流畅播放 + self.display_frame(frame) - # 在图像上绘制检测结果 - display_frame = self.draw_detections(frame.copy()) - - # 转换为Qt格式并显示 - self.display_frame(display_frame) - - # 更新右侧结果显示 - self.update_results_display() + # 如果当前没有在处理识别任务,则开始新的识别任务 + 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): """在图像上绘制检测结果""" @@ -335,14 +582,96 @@ class MainWindow(QMainWindow): def display_frame(self, frame): """显示帧到界面""" - 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) - - pixmap = QPixmap.fromImage(qt_image) - scaled_pixmap = pixmap.scaled(self.camera_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) - self.camera_label.setPixmap(scaled_pixmap) + 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): """更新右侧结果显示""" @@ -350,13 +679,8 @@ class MainWindow(QMainWindow): count = len(self.detections) self.count_label.setText(f"识别到的车牌数量: {count}") - # 清除之前的结果 - for i in reversed(range(self.results_layout.count())): - child = self.results_layout.itemAt(i).widget() - if child: - child.setParent(None) - - # 添加新的结果 + # 准备新的车牌结果列表 + new_plate_results = [] for i, detection in enumerate(self.detections): # 矫正车牌图像 corrected_image = self.correct_license_plate(detection) @@ -364,15 +688,53 @@ class MainWindow(QMainWindow): # 获取车牌号,传入车牌类型信息 plate_number = self.recognize_plate_number(corrected_image, detection['class_name']) - # 创建车牌显示组件 - plate_widget = LicensePlateWidget( - i + 1, - detection['class_name'], - corrected_image, - plate_number - ) + # 添加到新结果列表 + 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) - self.results_layout.addWidget(plate_widget) + # 添加新的结果 + 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): """矫正车牌图像""" @@ -390,40 +752,69 @@ class MainWindow(QMainWindow): ) def recognize_plate_number(self, corrected_image, class_name): - """识别车牌号""" - if corrected_image is None: - return "识别失败" - - try: - # 预测函数(来自模块) - # 函数名改成一样的了,所以不要修改这里了,想用哪个模块直接导入 - 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 "识别失败" + """识别车牌号""" + 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 lightCRNN_part.lightcrnn_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 lightCRNN_part.lightcrnn_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: + 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(): diff --git a/test_lpr_real_images.py b/test_lpr_real_images.py new file mode 100644 index 0000000..b3f859b --- /dev/null +++ b/test_lpr_real_images.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +LPRNet接口真实图片测试脚本 +测试LPRNET_part目录下的真实车牌图片 +""" + +import cv2 +import numpy as np +import os +from LPRNET_part.lpr_interface import LPRNinitialize_model, LPRNmodel_predict + +def test_real_images(): + """ + 测试LPRNET_part目录下的真实车牌图片 + """ + print("=== LPRNet真实图片测试 ===") + + # 初始化模型 + print("1. 初始化LPRNet模型...") + success = LPRNinitialize_model() + if not success: + print("模型初始化失败!") + return + + # 获取LPRNET_part目录下的图片文件 + lprnet_dir = "LPRNET_part" + image_files = [] + + if os.path.exists(lprnet_dir): + for file in os.listdir(lprnet_dir): + if file.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp')): + image_files.append(os.path.join(lprnet_dir, file)) + + if not image_files: + print("未找到图片文件!") + return + + print(f"2. 找到 {len(image_files)} 个图片文件") + + # 测试每个图片 + for i, image_path in enumerate(image_files, 1): + print(f"\n--- 测试图片 {i}: {os.path.basename(image_path)} ---") + + try: + # 使用支持中文路径的方式读取图片 + image = cv2.imdecode(np.fromfile(image_path, dtype=np.uint8), cv2.IMREAD_COLOR) + + if image is None: + print(f"无法读取图片: {image_path}") + continue + + print(f"图片尺寸: {image.shape}") + + # 进行预测 + result = LPRNmodel_predict(image) + print(f"识别结果: {result}") + print(f"识别车牌号: {''.join(result)}") + + except Exception as e: + print(f"处理图片 {image_path} 时出错: {e}") + import traceback + traceback.print_exc() + + print("\n=== 测试完成 ===") + +def test_image_loading(): + """ + 测试图片加载方式 + """ + print("\n=== 图片加载测试 ===") + + lprnet_dir = "LPRNET_part" + + if os.path.exists(lprnet_dir): + for file in os.listdir(lprnet_dir): + if file.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp')): + image_path = os.path.join(lprnet_dir, file) + print(f"\n测试文件: {file}") + + # 方法1: 普通cv2.imread + img1 = cv2.imread(image_path) + print(f"cv2.imread结果: {img1 is not None}") + + # 方法2: 支持中文路径的方式 + try: + img2 = cv2.imdecode(np.fromfile(image_path, dtype=np.uint8), cv2.IMREAD_COLOR) + # img2 = cv2.resize(img2,(128,48)) + print(f"cv2.imdecode结果: {img2 is not None}") + if img2 is not None: + print(f"图片尺寸: {img2.shape}") + except Exception as e: + print(f"cv2.imdecode失败: {e}") + +if __name__ == "__main__": + # 首先测试图片加载 + test_image_loading() + + # 然后测试完整的识别流程 + test_real_images() \ No newline at end of file