简介一份基于深度学习的车牌识别完整毕设项目内含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。模型体积上不要迷信大模型。车牌检测不是检测猫狗那种极度依赖语义的目标车牌本身具有强几何特征——蓝色或绿色底、白色字符、固定长宽比。YOLOv8nnano这种轻量模型在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 modelyolov8n.pt source./test.jpg verboseTrue这条命令会下载官方预训练权重并在test.jpg上做推理。verboseTrue会输出模型参数量、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识别用LPRNetGUI用PyQt5。三者都有大量开源实现组合起来既不过时也不至于复杂度失控。3. 跑通最小系统数据集准备、训练与推理串联的完整命令3.1 一次装对依赖Python环境与包管理这一节解决的是“环境搭了一个星期还没跑起来”的问题。先按python安装教程装好Python 3.9我实际测试下来3.8到3.10都能跑通但3.9的兼容问题最少。装完确认pip能执行然后用conda或venv建一个独立环境避免把系统Python搞乱。conda create -n plate python3.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用来做GUIonnxruntime后面提速会用到。参数说明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, encodingutf-8) as f: f.write(f0 {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再转YOLOimport 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(f0 {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}) with open(out_path, w, encodingutf-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 dataplate.yaml modelyolov8n.pt epochs80 imgsz640 batch16 patience15 device0逻辑说明modelyolov8n.pt表示在官方预训练基础上微调这比从零训练收敛快得多尤其是你只有几百张图的时候。train过程会在runs/detect/train下生成weights/best.pt和last.pt最后用best.pt做推理。参数说明imgsz640是推理和训练共用输入尺寸如果你的图片里车牌很小改成1280batch16在16G显存下够用显存小就降到8或4patience15表示验证集指标连续15轮不提升就早停防止过拟合。CPU训练时把device0改成devicecpu同时imgsz降到480速度会快很多但效果会略降。验证一下效果yolo detect predict modelruns/detect/train/weights/best.pt source./test.jpg conf0.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), dtypetorch.long) target_lengths torch.tensor([len(label) for label in labels], dtypetorch.long) targets torch.tensor( [char2idx[c] for label in labels for c in label], dtypetorch.long ) loss F.ctc_loss( logits, targets, input_lengths, target_lengths, blank0, zero_infinityTrue )逻辑说明F.ctc_loss是在logits上直接计算损失它内部会做log_softmax所以网络输出层不需要再手动加softmax。input_lengths全部取logits的序列长度因为每个裁剪图都被resize成同样宽度。target_lengths是每个车牌文本的真实字符数普通蓝牌是7新能源绿牌是8。参数说明blank0必须和codec.py里blank的索引一致。zero_infinityTrue的作用是当损失出现inf时把它置零防止某个极端样本把梯度炸掉。训练时输入高度固定为64宽度统一resize到240像素这是LPRNet的常用配置。学习率用1e-4优化器选AdamWbatch建议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(axis1) 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, conf0.5, verboseFalse)[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)否则模型输入维度错了推理会报错或者输出垃圾结果。conf0.5是检测阈值低于这个置信度的框会被过滤。到这里一个不依赖GUI的最小识别系统已经能跑了。接下来把它包装成能演示的窗口程序。4. 用PyQt5把模型包成GUI界面设计、线程隔离与打包交付4.1 GUI功能设计图片、视频、摄像头功能多做一项风险就多一分毕设演示时GUI不需要花哨但必须稳定。我建议功能清单控制在三块图片识别、视频识别、结果显示与保存。摄像头识别可以作为加分项但它在不同电脑上会触发驱动、权限、帧率问题演示现场一旦翻车很难收场所以我一般只做图片和视频。功能交互入口实现要点风险点图片识别“打开图片”按钮文件对话框选择图片显示原图图片过大时QPixmap内存占用高视频识别“打开视频”按钮按帧读取抽帧识别逐帧识别会卡死界面必须抽帧结果显示文本框/状态栏输出车牌号和置信度多车牌时显示顺序要稳定保存结果“保存结果”按钮把识别文本写成txt编码用UTF-8Windows记事本乱码参数说明视频识别不要每帧都跑模型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继承QThreadrun方法内执行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 字符全乱识别结果是“津Ax3”这种乱码现象检测框位置正常但识别出来的字符完全对不上汉字和字母位置错乱甚至出现、#等符号。原因几乎都是字符映射表不一致——训练模型时的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版二是显卡显存只有4Gbatch设成32还开1280分辨率训练。解决先跑通CPU全流程这比折腾GPU环境优先级高得多。检测用yolov8n、imgsz480、batch8CPU训练一两百张图只需要几十分钟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同样转ONNXCPU推理速度通常能提升20%到30%。如果演示机有NVIDIA显卡再用TensorRT量化推理帧率能从几帧提升到二十几帧但TensorRT的工程坑比较多提前一周做迁移测试。节点别搞错先量化识别模型因为它在视频识别里每帧都在跑检测模型一般跑一次也就几十毫秒优先调整imgsz和conf阈值。最后说一个我的习惯每轮测试把识别错误的裁剪图自动保存到hard_examples目录文件名带上“预测值_真值”。晚上花半小时翻一遍这些图比盲目调参有用得多。绝大多数失败样本集中在两类夜间强反光和车牌倾斜超过45度。把这些图补进训练集比改任何超参数都更奏效。这个习惯让我少走了很多弯路希望帮到你。本文还有配套的精品资源点击获取