简介:一份基于深度学习的车牌识别完整毕设项目,内含Python源码和图形界面,面向计算机、通信、人工智能、自动化等专业学生与从业者,可用于毕业设计、课程大作业及深度学习入门进阶。压缩包共2000个文件,以1942张JPG格式的车牌图像和40张PNG图片构成训练与测试数据,另含7个PY源程序、7个XML配置文件及MD说明文档,整体大小约265.63MB。该项目已在答辩评审中获得98分,代码经过调试测试确保可运行,已有259人学习浏览。下载后可获得完整的车牌识别流程实现、可视化操作界面、数据集与标注文件,既能帮助初学者理解深度学习模型的训练与预测逻辑,也便于在现有基础上修改调整、拓展识别场景或集成到其他系统中。图形界面支持图片选择与结果展示,代码采用模块化设计,便于分别学习车牌定位、字符分割与识别等环节,附带的真实车牌图像覆盖多种号码格式,可直接用于模型效果验证。
1. 深度学习车牌识别项目到底在做什么:从道闸识别失败说起
基于深度学习的车牌识别源码加GUI界面,是Python毕业设计里最常见也最容易被低估的一个项目。常见场景是这样的:雨天傍晚,停车场道闸的摄像头把一辆绿牌车拍成了“反光块”,杆子迟迟不放行,后排车开始按喇叭。问题不在摄像头,而在识别逻辑沿用老办法——先颜色过滤、再边缘检测、最后按字符切分,光照一差就全线崩盘。深度学习的做法是把这件事拆成两个模型:先用目标检测定位车牌区域,再用字符序列识别把车牌号读出来,最后用一个PyQt5窗口把这些能力暴露给用户操作。
这个项目能解决的问题很实在:一张普通图片进去,输出车牌框和车牌文本,支持图片、视频和摄像头输入。它适合三类人:要交毕设但不想只写理论的学生、想熟悉“训练—部署—打包”全流程的Python入门者、以及想快速搭一个演示版识别系统的开发者。下面按可复现的路径拆,默认你手上有一份源码包,但重点不依赖任何私有代码,全部用公开模型和数据也能跑通。
2. 为什么是YOLO加字符序列识别:两段式方案的选型逻辑
2.1 车牌识别不是OCR直接识字:先定位再识别才是主流
很多新手拿到需求,第一反应是拿OCR模型直接对整张图做文字识别。这在实际场景里几乎必翻车,原因很直白:在车辆原图里,车牌区域只占画面的几个百分点。OCR模型要在全图范围搜索小目标字符,不仅算力浪费,还容易被车灯、车身贴纸、护栏上的广告文字干扰。
主流方案是两段式:第一段用YOLO这类目标检测网络找出“车牌”这个物体,输出一个矩形框;第二段把框内的图像裁出来,单独送给字符识别模型。这样做的好处是职责清晰——检测模型只回答“车牌在哪”,识别模型只回答“这几个字符是什么”。两个模型可以独立训练、独立调参,哪一个效果差就补哪一个,不用互相牵连。
我自己做过一个对比实验:同一个测试集,直接整图OCR的字符准确率不到40%,而“YOLO检测+LPRNet识别”能到90%以上。差距主要不在模型能力,而在分工。车牌识别这个任务天然适合两步走,因为检测阶段能过滤掉大部分背景噪声,识别阶段面对的输入已经非常干净。
| 方案 | 输入尺寸 | 背景鲁棒性 | 训练成本 | 调参难度 |
|---|---|---|---|---|
| 整图OCR | 大(全图) | 差 | 高,需要海量全图标注 | 高 |
| YOLO检测+LPRNet识别 | 小(只裁车牌) | 好 | 中,两段分开训练 | 低 |
| 端到端(检测+识别一个模型) | 中 | 中 | 高,数据要求严苛 | 高 |
表里的端到端方案,比如最近几年的针对交通场景的统一模型,学术上很热,但要自己复现训练,数据标注格式、损失函数组合、学习率调度都要重新摸索,对毕设周期不友好。两段式是“能交差、能答辩、能改”的最优解。
2.2 YOLO选型:v5、v8、nano还是s,别一上来就用大模型
目标检测部分我建议在YOLOv8和YOLOv5之间选。v8的ultralytics包API更现代,训练和推理都只需要几行代码;v5文档和第三方教程更多,遇到问题更容易搜到答案。对新项目,我一般直接用v8。
模型体积上,不要迷信大模型。车牌检测不是检测猫狗那种极度依赖语义的目标,车牌本身具有强几何特征——蓝色或绿色底、白色字符、固定长宽比。YOLOv8n(nano)这种轻量模型在CPU上跑都能有不错效果。我自己测试过,nano和small在车牌检测上的mAP差距通常不超过2个点,但nano的推理速度快一倍以上。毕业设计演示机器往往是笔记本CPU,选nano或small是务实选择。
几个关键参数我习惯这么设:输入尺寸imgsz默认640;如果摄像头画面中车辆离得远、车牌占比很小,就调到1280,但推理时间会明显上涨。置信度阈值conf在演示时设0.45到0.5比较合适,太低会把车身上的“XX 4S店”铭牌当车牌框出来。IoU阈值iou固定0.45,一般不用动。
用ultralytics打印一个YOLO模型的参数,可以直观看到网络规模:
yolo detect predict model=yolov8n.pt source=./test.jpg verbose=True这条命令会下载官方预训练权重并在test.jpg上做推理。verbose=True会输出模型参数量、FLOPs、各层耗时。第一次跑可以先看这些数字,对CPU设备能跑多快心里有个底。参数说明:yolov8n.pt是带预训练权重的模型文件,source可以指向单张图、目录或视频文件;如果机器没有外网,需要提前把权重放到models目录里。
2.3 字符识别不走切分老路:LPRNet和CTC损失
车牌字符识别的核心矛盾是“不切分就难对齐”。传统方法先做字符切分——把“京A12345”拆成单个字符,再逐个分类。听着简单,但车牌上常有铆钉、污渍、倾斜、反光,投影切分法一遇到字符粘连就直接废掉。
LPRNet这类序列识别模型绕开了切分。它的结构可以大致理解为:CNN主干提取图像特征,把特征图压成时间序列,再接RNN建模字符之间的上下文依赖,最后用CTC损失对齐“图像序列”和“文本标签”。训练时你只需要给它“整张车牌图片+完整车牌号”,不需要标注每个字符的位置。推理时CTC解码会自动去掉空白帧和重复字符,输出一个字符串。
这背后的原理建议找一本深度学习课本的pdf对照着看,重点是理解RNN的时序建模和CTC的对齐机制。不补这块,后面调识别模型的参数基本靠玄学,因为你不清楚loss在优化什么。
中国车牌字符集有个固定约束:省份简称31个汉字,字母24个(去掉I和O,避免和数字1、0混淆),数字10个,再加上CTC用的blank空位,一共66类。这也是识别模型输出层的神经元数量。自己做字符映射表时,顺序一定要和训练时完全一致,否则推理结果全是乱码——这个坑我后面专门讲。
2.4 备选方案对比:PaddleOCR和端到端模型为什么不适合毕设
有人会问,PaddleOCR现在这么成熟,为什么不直接用它读车牌?PaddleOCR对印刷体、自然场景文字的通用识别能力确实强,但车牌字符有独特问题:字体和通用中文字库不完全一致,牌照边框、铆钉会干扰文本检测,而且PaddleOCR是通用检测加识别,流程重,部署体积大。直接套用通常要微调,微调成本比从零训LPRNet还麻烦。
端到端模型(一个网络同时输出框和文本)学术效果好,但训练时需要检测框和字符级标注同时对齐,数据准备复杂,训练技巧多,loss经常要到几千轮才收敛。对于“能演示、能答辩、能讲清楚”的毕设项目,成本不划算。
我的选型结论是固定的:检测用YOLOv8n或YOLOv8s,识别用LPRNet,GUI用PyQt5。三者都有大量开源实现,组合起来既不过时,也不至于复杂度失控。
3. 跑通最小系统:数据集准备、训练与推理串联的完整命令
3.1 一次装对依赖:Python环境与包管理
这一节解决的是“环境搭了一个星期还没跑起来”的问题。先按python安装教程装好Python 3.9,我实际测试下来3.8到3.10都能跑通,但3.9的兼容问题最少。装完确认pip能执行,然后用conda或venv建一个独立环境,避免把系统Python搞乱。
conda create -n plate python=3.9 -y conda activate plate pip install torch torchvision pip install ultralytics opencv-python pyqt5 onnxruntime tqdm numpy逻辑说明:torch安装会默认选择适合当前机器的版本,如果你的机器没有NVIDIA显卡,安装的就是CPU版,一样能训练小模型,只是慢;有显卡的话,先运行nvidia-smi查看驱动支持的CUDA版本,再去torch官网选对应版本安装,不要盲目装最新版。ultralytics是YOLOv8的官方封装包,opencv负责图像读写和预处理,pyqt5用来做GUI,onnxruntime后面提速会用到。
参数说明:conda和venv二选一,我推荐conda,因为PyQt5在某些环境下依赖比较复杂,conda能一并管理。装完后用vscode做python环境配置,选择plate解释器再执行pip,否则容易装到另一个环境里,后面import torch都会失败。检查环境的命令:
python -c "import torch, cv2; print(torch.__version__, cv2.__version__)"如果输出两个版本号,说明环境基本可用。这里最容易踩的坑是“明明pip install成功了,但运行时找不到包”,十有八九是vscode没有切换解释器。
3.2 数据准备:CCPD大样本与LabelImg自标注两条路
车牌检测数据有两个来源。大规模训练用CCPD数据集,它的图片按天气、角度、模糊等场景做了划分,图片数量足够覆盖真实情况。但CCPD的标注是编码在文件名里的,需要写脚本解析出来转成YOLO的txt标签。转换脚本网上很多,但不同版本字段顺序可能不一样,核心思路是识别文件名中“四个数字坐标组成的一段”,那段就是车牌框的左上角和右下角。
import os import glob def parse_ccpd_box(filename): """从CCPD文件名中解析车牌框坐标,返回 [x1, y1, x2, y2]""" stem = os.path.basename(filename).replace(".jpg", "") parts = stem.split("-") for part in parts: items = part.split("_") if len(items) == 4: try: nums = [int(v) for v in items] except ValueError: continue # 车牌框满足:x1 < x2 且 y1 < y2 if nums[0] < nums[2] and nums[1] < nums[3]: return nums return None def convert_to_yolo(filename, out_txt, img_w, img_h): box = parse_ccpd_box(filename) if box is None: return False x1, y1, x2, y2 = box xc = (x1 + x2) / 2 / img_w yc = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h with open(out_txt, "w", encoding="utf-8") as f: f.write(f"0 {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}\n") return True逻辑说明:parse_ccpd_box遍历文件名用“-”切出的每一段,找形如“x1_y1_x2_y2”的那段。这里用条件“左上小于右下”做二次校验,能避开文件名里其他坐标段。拿到绝对坐标后再除以图片宽高,转成YOLO需要的相对坐标,类别统一写0,表示“plate”这一类别。
参数说明:img_w和img_h必须和图片实际尺寸一致,可以从OpenCV读取或者取CCPD的默认分辨率。如果你手上的CCPD版本解析出来坐标明显不对,最直接的验证办法是随机找一张图,把解析出的坐标画上去看框的位置,框不对就换一个解析脚本。
如果你只是快速验证训练流程,不想啃CCPD,可以用LabelImg手动标注一两百张车牌图,导出VOC格式的xml,再转YOLO:
import xml.etree.ElementTree as ET def voc2yolo(xml_path, out_path, img_w, img_h): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in ("plate", "license_plate"): continue bnd = obj.find("bndbox") x1 = float(bnd.find("xmin").text) y1 = float(bnd.find("ymin").text) x2 = float(bnd.find("xmax").text) y2 = float(bnd.find("ymax").text) xc = (x1 + x2) / 2 / img_w yc = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"0 {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}") with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(lines))逻辑说明:VOC标注里每个object是一个标注框,这里只保留类别为plate或license_plate的框,忽略车身其他部分。转换公式是固定套路,把左上右下坐标变成中心点坐标加宽高,再全部除以图片尺寸归一化。
参数说明:LabelImg标注时矩形框要和车牌边缘贴合,不要留太多车身背景,尤其是不要包含车灯。训练样本的框质量直接影响检测精度,这一点比数据量更重要。
3.3 训练车牌检测模型:YOLO的命令与参数
数据准备好后,先建一个yaml描述数据路径和类别。image和label目录要一一对应,图片叫0001.jpg,标签就叫0001.txt。
# plate.yaml train: ./data/ccpd_yolo/train.txt val: ./data/ccpd_yolo/val.txt nc: 1 names: ['plate']参数说明:train.txt和val.txt里每行是一张图片的绝对路径,YOLO会自动在同目录下找同名txt标签。nc是类别数,这里只有车牌一个类别。names里的名字要和你转换脚本里写的类别id对应。
训练命令:
yolo detect train data=plate.yaml model=yolov8n.pt epochs=80 imgsz=640 batch=16 patience=15 device=0逻辑说明:model=yolov8n.pt表示在官方预训练基础上微调,这比从零训练收敛快得多,尤其是你只有几百张图的时候。train过程会在runs/detect/train下生成weights/best.pt和last.pt,最后用best.pt做推理。
参数说明:imgsz=640是推理和训练共用输入尺寸,如果你的图片里车牌很小,改成1280;batch=16在16G显存下够用,显存小就降到8或4;patience=15表示验证集指标连续15轮不提升就早停,防止过拟合。CPU训练时把device=0改成device=cpu,同时imgsz降到480,速度会快很多,但效果会略降。验证一下效果:
yolo detect predict model=runs/detect/train/weights/best.pt source=./test.jpg conf=0.53.4 训练LPRNet字符识别模型:CTC loss的关键写法
识别模型的训练数据格式比检测简单:每行一张裁剪好的车牌小图路径,后面跟车牌文本,用tab分隔。例如“crops/京A12345.jpg 京A12345”。裁剪图可以从检测得到的框里切出来,也可以用手动标注的框切。
字符映射表必须单独放一个文件,训练和推理共用:
# codec.py provinces = list("京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新") letters = [c for c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" if c not in "IO"] digits = [str(i) for i in range(10)] chars = ["blank"] + provinces + letters + digits char2idx = {c: i for i, c in enumerate(chars)} idx2char = {i: c for i, c in enumerate(chars)}参数说明:省份简称固定31个,字母去掉I和O是因为车牌设计上就不使用这两个字母,去掉它们能减少和数字1、0的混淆。chars列表的第一个元素是blank,留给CTC做空位,索引为0。这个文件的顺序一旦确定就不要改,后面训练和推理都从它导入。
训练主循环里最需要注意的是CTC loss的输入输出长度对齐:
import torch import torch.nn.functional as F # logits: [seq_len, N, num_classes],由LPRNet网络输出 # labels: 一个batch的车牌文本列表,例如 ["京A12345", "沪B88888"] input_lengths = torch.full((N,), logits.size(0), dtype=torch.long) target_lengths = torch.tensor([len(label) for label in labels], dtype=torch.long) targets = torch.tensor( [char2idx[c] for label in labels for c in label], dtype=torch.long ) loss = F.ctc_loss( logits, targets, input_lengths, target_lengths, blank=0, zero_infinity=True )逻辑说明:F.ctc_loss是在logits上直接计算损失,它内部会做log_softmax,所以网络输出层不需要再手动加softmax。input_lengths全部取logits的序列长度,因为每个裁剪图都被resize成同样宽度。target_lengths是每个车牌文本的真实字符数,普通蓝牌是7,新能源绿牌是8。
参数说明:blank=0必须和codec.py里blank的索引一致。zero_infinity=True的作用是当损失出现inf时把它置零,防止某个极端样本把梯度炸掉。训练时输入高度固定为64,宽度统一resize到240像素,这是LPRNet的常用配置。学习率用1e-4,优化器选AdamW;batch建议64。如果训练集字符分布不均衡,汉字省份只有几百张而字母数字有几万张,可以考虑用WeightedRandomSampler平衡采样,否则模型会对汉字严重欠拟合。
3.5 推理串联:检测出框、裁剪、识别、CTC解码
训练完两个模型,真正用起来要写一个串联脚本。这段代码是后面GUI内部调用的核心函数,值得仔细看:
import cv2 import numpy as np import torch from ultralytics import YOLO from codec import idx2char def preprocess_crop(crop_bgr): """把检测框裁剪图转成LPRNet输入张量""" gray = cv2.cvtColor(crop_bgr, cv2.COLOR_BGR2GRAY) resized = cv2.resize(gray, (240, 64)) # 注意先宽后高 img = resized.astype(np.float32) / 255.0 tensor = torch.from_numpy(img).unsqueeze(0).unsqueeze(0) return tensor def decode_ctc(probs): """CTC贪心解码:合并重复字符,去掉blank""" preds = probs.argmax(axis=1) output = [] prev = None for p in preds: if p != prev and p != 0: output.append(idx2char[p.item()]) prev = p return "".join(output) def run_infer(frame_bgr, det_model, rec_model): results = det_model(frame_bgr, conf=0.5, verbose=False)[0] boxes = results.boxes.xyxy.cpu().numpy() output = [] for x1, y1, x2, y2 in boxes.astype(int): crop = frame_bgr[y1:y2, x1:x2] if crop.size == 0: continue tensor = preprocess_crop(crop) with torch.no_grad(): logits = rec_model(tensor) # [seq_len, 1, num_classes] probs = logits.squeeze(1) # [seq_len, num_classes] text = decode_ctc(probs) output.append((text, [x1, y1, x2, y2])) return output逻辑说明:det_model返回的结果中boxes.xyxy直接是像素坐标。裁剪出来的小图先转灰度、resize到240x64,再归一化到0到1之间。归一化参数这里取了最简单的方式,如果预训练权重使用了特定均值和标准差,需要用同一个值,否则识别效果会明显变差。decode_ctc先对每个时间步取概率最大的字符索引,然后按CTC规则合并连续重复字符、删除索引0的blank,得到最终车牌字符串。
参数说明:resize目标尺寸的写法是(width, height),也就是(240, 64),别写成(64, 240),否则模型输入维度错了,推理会报错或者输出垃圾结果。conf=0.5是检测阈值,低于这个置信度的框会被过滤。
到这里,一个不依赖GUI的最小识别系统已经能跑了。接下来把它包装成能演示的窗口程序。
4. 用PyQt5把模型包成GUI:界面设计、线程隔离与打包交付
4.1 GUI功能设计:图片、视频、摄像头,功能多做一项风险就多一分
毕设演示时,GUI不需要花哨,但必须稳定。我建议功能清单控制在三块:图片识别、视频识别、结果显示与保存。摄像头识别可以作为加分项,但它在不同电脑上会触发驱动、权限、帧率问题,演示现场一旦翻车很难收场,所以我一般只做图片和视频。
| 功能 | 交互入口 | 实现要点 | 风险点 |
|---|---|---|---|
| 图片识别 | “打开图片”按钮 | 文件对话框选择图片,显示原图 | 图片过大时QPixmap内存占用高 |
| 视频识别 | “打开视频”按钮 | 按帧读取,抽帧识别 | 逐帧识别会卡死界面,必须抽帧 |
| 结果显示 | 文本框/状态栏 | 输出车牌号和置信度 | 多车牌时显示顺序要稳定 |
| 保存结果 | “保存结果”按钮 | 把识别文本写成txt | 编码用UTF-8,Windows记事本乱码 |
参数说明:视频识别不要每帧都跑模型,15到25帧每秒的视频每帧跑一次,4核CPU机器大概率卡成PPT。我一般在视频线程里加一个时间间隔,每200到300毫秒抽一帧识别,既能看到连续效果,又不会把GUI拖死。
4.2 界面布局代码:不用Qt Designer,直接代码布控
对于这种功能明确的工具,用Qt Designer还要多维护一个ui文件,直接代码布局更直观,也方便答辩时讲。核心窗口控件只需要三个:一个显示图像的QLabel、一个显示结果的QTextEdit、三个按钮。
import sys from PyQt5.QtWidgets import ( QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QFileDialog, QTextEdit ) from PyQt5.QtCore import Qt class PlateApp(QWidget): def __init__(self): super().__init__() self.setWindowTitle("车牌识别系统") self.resize(900, 600) self.pic_label = QLabel("未加载图片") self.pic_label.setAlignment(Qt.AlignCenter) self.pic_label.setStyleSheet("background:#f0f0f0;border:1px solid #ccc;") self.txt_result = QTextEdit() self.txt_result.setReadOnly(True) self.btn_open = QPushButton("打开图片") self.btn_video = QPushButton("打开视频") self.btn_start = QPushButton("开始识别") layout = QVBoxLayout() layout.addWidget(self.pic_label) layout.addWidget(self.txt_result) btn_layout = QHBoxLayout() btn_layout.addWidget(self.btn_open) btn_layout.addWidget(self.btn_video) btn_layout.addWidget(self.btn_start) layout.addLayout(btn_layout) self.setLayout(layout)逻辑说明:pic_label用来显示原始图片和画好检测框的结果图,txt_result显示识别出的车牌号和置信度,三个按钮分别绑定槽函数。布局用的是垂直布局套水平布局,缩放窗口时图像区域会跟着变大。
参数说明:setReadOnly(True)防止用户误改识别结果。resize(900, 600)是初始窗口大小,分辨率低于1366x768的笔记本上也能完整显示。显示图片时要把OpenCV的BGR数据转成RGB,再从numpy数组转成QImage,这一步漏了,图片颜色会偏蓝,答辩时很显眼。
4.3 用QThread把推理移出主线程:界面卡死和崩溃的根源
PyQt的主线程负责界面事件循环,如果在按钮的clicked槽里直接跑模型推理,模型300毫秒的耗时虽然不长,但界面会进入“未响应”状态,Windows会在几秒后弹出“是否关闭程序”的提示。正确做法是让推理在子线程中执行,识别完成后再通过信号把结果传回主线程。
from PyQt5.QtCore import QThread, pyqtSignal class InferWorker(QThread): result_ready = pyqtSignal(object, object) # (识别结果, 图像) def __init__(self, frame, det_model, rec_model): super().__init__() self.frame = frame self.det_model = det_model self.rec_model = rec_model def run(self): output = run_infer(self.frame, self.det_model, self.rec_model) vis = draw_boxes(self.frame, output) self.result_ready.emit(output, vis)逻辑说明:InferWorker继承QThread,run方法内执行run_infer,结果通过pyqtSignal发出去。主线程里的按钮槽只做两件事:禁用按钮、启动线程;当result_ready信号到达时,再把结果显示到界面上。这样界面在整个识别过程中保持流畅。
参数说明:result_ready携带两个对象,第一个是识别结果列表,第二个是画了检测框的图像。draw_boxes是自定义函数,负责把车牌框画到原图上,建议用OpenCV的rectangle绘制,颜色选绿色,线宽2到3像素,这样投影到教室屏幕上也能看清。
4.4 模型路径与打包交付:exe找不到best.pt怎么办
GUI写完后,最常遇到的问题不是代码报错,而是打包后双击exe提示找不到模型文件。原因是PyInstaller打包时会把资源文件放在临时解压目录,和源码运行时的相对路径不一样。写一个resource_path函数统一处理:
import sys import os def resource_path(relative_path): if hasattr(sys, "_MEIPASS"): return os.path.join(sys._MEIPASS, relative_path) return os.path.join(os.path.dirname(os.path.abspath(__file__)), relative_path) det_model_path = resource_path("models/best.pt") rec_model_path = resource_path("models/lprnet.pt")逻辑说明:源码运行时,资源路径取脚本所在目录;PyInstaller打包后,资源会被解压到_MEIPASS临时目录,resource_path会自动切换。模型加载全部用resource_path拼路径,而不是直接写“models/best.pt”,这是打包最容易忽略的细节。
打包命令:
pyinstaller -F -w main.py逻辑说明:-F表示打包成单个exe文件,-w表示不显示控制台窗口。首次打包会生成spec文件,如果之后要加模型资源,可以在spec的datas里加上(“models”, “models”),把整个models目录带进exe。
参数说明:演示机没有装Python环境时,-F是必须的。但打包单文件exe启动慢,因为要解压资源到临时目录。如果答辩需要频繁演示,我建议用-D目录模式打包,启动速度更快,调试也更方便。另外,杀毒软件对PyInstaller打包的exe经常误报,提前在演示电脑上加入白名单,别等答辩现场才排查。
5. 避坑:车牌识别项目最常见的5个坑与排查路径
5.1 字符全乱:识别结果是“津A:x3@@”这种乱码
现象:检测框位置正常,但识别出来的字符完全对不上,汉字和字母位置错乱,甚至出现@、#等符号。原因几乎都是字符映射表不一致——训练模型时的chars列表顺序和推理时的不一致。别人给源码时附带了一个训练好的pth权重,但没附对应的codec.py;或者你改了codec.py里的字符顺序,导致同样的权重在同一个索引上读出了不同字符。
解决:训练和推理必须共用同一个codec.py文件,不要复制粘贴到另一个目录再改。拿到一个陌生权重时先验证:找一张已知车牌图,例如“京A12345”,推理打印出argmax的原始索引序列,然后对照你的idx2char看是不是“京A12345”。如果对不上,说明idx2char的顺序和权重不一致。最省事的办法是直接重训一个识别模型,反正LPRNet在几千张图上几十轮就能收敛。
5.2 新能源绿牌总是少一位:8字符被识别成7字符
现象:普通蓝牌“京A12345”识别没问题,但新能源绿牌“京AD12345”这类8位车牌,经常只识别出前7位,最后一位丢掉或者识别成空白。原因是训练数据里蓝牌占绝大多数,模型对8字符序列的建模能力不足;另一个原因是解码后处理把结果硬截断成7位。
解决:先在推理代码里删掉任何固定截断逻辑。车牌长度可以是7位也可以是8位,后处理应该按CTC解码结果自然输出。然后检查LPRNet输出序列长度是否足够,假设网络对高度64、宽度240的输入产生60到70个时间步,7字符和8字符的车牌都有冗余空间,如果序列长度只有20,那8个字符根本装不下,需要调整网络的下采样倍数。最后是数据层面,训练集里要按比例加入新能源车牌,至少占20%,否则模型就是没见过8位字符的排列。
5.3 GUI一运行就未响应:推理占住了主线程
现象:点“开始识别”后窗口标题变成“未响应”,过几秒Windows提示是否强制关闭;识别完成前界面没有任何反馈。原因就是我在4.3里说的,推理放在按钮的clicked槽函数里同步执行,模型计算阻塞了主线程的事件循环。
解决:把所有推理挪进QThread,用signal把结果传回主线程。视频识别时还要额外注意,不要在循环里每帧都start一个新的QThread,线程创建和销毁的开销会让程序越来越卡。正确做法是视频识别里只启动一个常驻线程,线程内部按时间间隔抽帧识别。
5.4 CUDA装不上、显存不够:训练环境比模型更折磨人
现象:pip安装torch成功,但运行代码时torch.cuda.is_available()返回False;或者训练到第3轮报CUDA out of memory。原因分两种:一是PyPI默认安装的是CPU版torch,装之前没换CUDA版;二是显卡显存只有4G,batch设成32还开1280分辨率训练。
解决:先跑通CPU全流程,这比折腾GPU环境优先级高得多。检测用yolov8n、imgsz=480、batch=8,CPU训练一两百张图只需要几十分钟;LPRNet识别模型CPU推理单张也就几十毫秒,毕设演示完全扛得住。GPU环境的坑确认两点再装:nvidia-smi看驱动支持的最高CUDA版本,然后装对应版本的torch。显存不够就减小batch,不要换大模型。
5.5 识别准确率上不去:loss不降、汉字全错、数字对但省份错
现象:训练了好几十轮,整体loss下降但字符准确率卡在70%左右;数字和字母基本正确,省份简称汉字大面积识别错。原因主要是两个:一个字符类别严重不平衡,31个汉字每个出现频率远低于数字和字母;另一个是学习率设置太高,LSTM部分和CNN部分的训练节奏不同,统一用一个学习率容易让字符头跑飞。
解决:学习率从默认的1e-3降到1e-4,优化器换成AdamW,权重衰减设1e-4。对训练数据做采样平衡,把每个汉字类别的样本数控制到接近。还有一个容易被忽略的点:检测框裁剪出来的车牌图不要包含太多边缘和背景,LPRNet输入是灰度图,背景噪声会直接参与特征提取,裁剪框向外扩2到3个像素就够了,画面里车身颜色占比过高会掩盖字符特征。训练时加一点随机旋转和数据增强,模拟倾斜车牌,能明显改善汉字识别率。
6. 进阶验证:识别率怎么测、模型怎么提速、失败样本怎么用
一个模型部署前必须做分级验证。把测试集按夜间、模糊、倾斜、远距离分成四组,每组各100张图跑一遍,统计车牌级准确率,而不是只报一个整体值。整体90%会掩盖夜间只有60%的事实,也让你在答辩时说不出“模型适合什么场景、不适合什么场景”这种关键问题。统计脚本很简单:每张图跑run_infer,把识别文本和标注文本做字符串相等比较,按组累加。
import os import json groups = ["night", "blur", "tilt", "far"] result = {g: {"total": 0, "correct": 0} for g in groups} for name in os.listdir(test_dir): group = name.split("_")[0] if group not in result: continue frame = cv2.imread(os.path.join(test_dir, name)) output = run_infer(frame, det_model, rec_model) predict = output[0][0] if output else "" label = name.split(".")[0].split("_")[1] result[group]["total"] += 1 if predict == label: result[group]["correct"] += 1逻辑说明:测试集文件名用“场景_车牌号.jpg”的规则命名,脚本按文件名前缀分组统计。predict取第一个检测框的结果,超过一个车牌的图不在这个简单脚本范围内。
提速方面,我的常规顺序是:检测模型导出ONNX用onnxruntime推理,LPRNet同样转ONNX,CPU推理速度通常能提升20%到30%。如果演示机有NVIDIA显卡,再用TensorRT量化推理,帧率能从几帧提升到二十几帧,但TensorRT的工程坑比较多,提前一周做迁移测试。节点别搞错:先量化识别模型,因为它在视频识别里每帧都在跑;检测模型一般跑一次也就几十毫秒,优先调整imgsz和conf阈值。
最后说一个我的习惯:每轮测试把识别错误的裁剪图自动保存到hard_examples目录,文件名带上“预测值_真值”。晚上花半小时翻一遍这些图,比盲目调参有用得多。绝大多数失败样本集中在两类:夜间强反光和车牌倾斜超过45度。把这些图补进训练集,比改任何超参数都更奏效。这个习惯让我少走了很多弯路,希望帮到你。
本文还有配套的精品资源,点击获取