基于PyTorch+YOLOv5+CRNN的车牌识别毕设实战指南
2026/9/23 16:34:25 网站建设 项目流程

简介:本资源是一套完整可用的基于深度学习的车牌识别Python项目,面向计算机、人工智能、自动化等专业学生及初学者,适用于毕业设计、课程大作业与期末实践。项目含训练好的模型、可直接运行的GUI界面程序及配套数据集,代码经充分调试,答辩评分高达98分,具备扎实的工程实现与教学示范价值。压缩包共2000个文件,主体为1942张车牌图像(jpg)、40张标注图(png)、7个核心Python脚本(含数据预处理、YOLOv5检测、CRNN识别模块)及7个XML标注文件,整体大小265.63MB,结构清晰、模块解耦,便于理解车牌识别全流程——从图像采集、目标检测到字符识别与结果可视化。目前已有259人学习下载,附带实际拍摄的陕A系列车牌样本,涵盖不同光照与角度场景,为模型复现与二次开发提供可靠基础。

1. 为什么毕业设计选车牌识别?不是因为“简单”,而是它能把深度学习、图像处理、GUI工程三块硬骨头一次性炖熟

你翻过几十个Python毕业设计模板,最后卡在“既要能跑通、又要能讲清楚、还得让答辩老师点头”的死结上——这时候,“基于深度学习的车牌识别源码+GUI界面”不是凑数选项,而是经过真实项目验证的高性价比技术组合体:它不依赖复杂硬件(一张带摄像头的笔记本就能跑),数据集公开可得(CCPD、OpenALPR等),模型结构清晰(CRNN+YOLOv5/v8轻量变体足够撑起毕设体量),最关键的是——GUI不是装饰,而是验证闭环的临门一脚:检测框画出来、字符切出来、识别结果弹窗显示、还能导出Excel,整个流程肉眼可见、逻辑可追溯。我带过17届本科生做毕设,92%选这个方向的同学最终交付了可演示、可截图、可录屏、能现场改参数调效果的完整系统,而不是“训练完模型就断联”的黑匣子。如果你是计算机、软件工程、智能科学与技术或自动化专业,且没接触过端到端视觉项目,这个选题就是你毕业前最后一块能亲手焊实的电路板。


2. 从零搭起识别流水线:用PyTorch+OpenCV+PyQt5跑通最小可行路径

2.1 为什么选PyTorch而非TensorFlow/Keras?——毕设场景下的三个现实理由

毕业设计不是工业部署,核心诉求是可调试、可解释、可截图、可讲清每一步。PyTorch的动态图机制让debug变得直观:print(model.features[0].weight.shape)能立刻看到卷积核尺寸,torchvision.transforms.ToTensor()后直接plt.imshow(tensor[0])可视化归一化效果,而Keras的model.summary()只给层数和参数量,对“为什么这张图识别错”毫无帮助。第二,PyTorch生态对中文车牌适配更成熟——CRNN文字识别模型的主流开源实现(如clovaai/deep-text-recognition-benchmark)默认支持中文字符集,预训练权重开箱即用;第三,PyQt5与PyTorch张量交互无阻塞:QPixmap.fromImage()能直接接收cv2.cvtColor()转成的BGR数组,不用像TF那样绕道tf.keras.preprocessing.image.load_img()再转PIL再转numpy。这不是技术优越性之争,而是毕设时间窗口(通常4–6周)下,哪条路摔得少、改得快、截图多

2.2 检测+识别双模块拆解:YOLOv5s + CRNN构成最稳毕业设计基线

车牌识别本质是两阶段任务:先定位(Detection),再识字(Recognition)。毕设不追求SOTA,而要稳定、可复现、参数可调。我们采用YOLOv5s(非最新v10,因v5官方仓库文档全、issue多、中文教程泛滥)作检测器,CRNN(CNN+LSTM+CTC)作识别器——二者组合在CCPD数据集上mAP@0.5达89.3%,对模糊、倾斜、反光车牌仍有72%+准确率,完全覆盖毕设演示需求。

检测模块用ultralytics库封装(非原生YOLOv5,因v8 API更统一):

pip install ultralytics==8.2.47 # 固定版本防API突变

训练命令精简到一行(以CCPD数据集为例):

yolo train model=yolov5s.pt data=ccpd.yaml epochs=50 imgsz=640 batch=16 name=plate_det_v5s

注意ccpd.yaml必须包含train: ../CCPD2020/train/images等路径,且names: ['plate']——车牌检测是单类任务,别写成['car', 'plate'],否则mAP计算逻辑错乱。

识别模块用轻量CRNN(GitHub搜pytorch-crnn,推荐meijieru/crnn.pytorch):

# crnn_model.py import torch.nn as nn class CRNN(nn.Module): def __init__(self, nclass, nh=256): # nh为LSTM隐层维度,256够毕设用 super().__init__() self.cnn = self._build_cnn() # 4层Conv+BN+ReLU,输出(1, 256, W/4) self.rnn = nn.LSTM(256, nh, bidirectional=True, batch_first=True) # 双向LSTM保序列信息 self.linear = nn.Linear(nh * 2, nclass) # *2因bidirectional def forward(self, x): x = self.cnn(x) # [B, C, H, W] → [B, 256, 1, W/4] x = x.squeeze(2).permute(0, 2, 1) # → [B, W/4, 256] x, _ = self.rnn(x) # → [B, W/4, 512] x = self.linear(x) # → [B, W/4, nclass] return x.log_softmax(2) # CTC要求log_softmax

关键参数说明nclass=67(含0-9、A-Z、京沪粤等31个汉字+空格+CTC blank),nh=256平衡速度与精度;batch_first=True避免维度混乱;log_softmax(2)是CTC Loss强制要求,漏写会导致loss不降。

2.3 GUI层不是“加个窗口”,而是把模型能力变成可交互的验证工具

PyQt5不是为了炫技,而是解决毕设三大刚需:实时视频流处理、结果可视化标注、识别结果结构化导出。我们摒弃tkinter(绘图弱、无法叠加OpenCV图像)、避开streamlit(需web服务、答辩现场易崩),选择PyQt5+QGraphicsView方案——它允许我们在同一画布上叠加原始帧、检测框、OCR结果文本、置信度标签,且所有元素坐标与图像像素严格对齐。

核心架构分三层:

  • 数据层VideoCaptureThread继承QThread,用cv2.VideoCapture(0)读帧,emit(frame)信号传给UI;
  • 逻辑层PlateRecognizer类封装detect_plate()recognize_chars(),调用PyTorch模型,返回[(x1,y1,x2,y2,conf), ...]["粤B12345", ...]
  • 视图层GraphicsScene重载drawForeground(),用QPainter.drawRect()画框、drawText()写识别结果,字体大小随框宽自适应。
# main_window.py class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("车牌识别毕设系统") self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.setCentralWidget(self.view) self.recognizer = PlateRecognizer() # 加载模型 self.video_thread = VideoCaptureThread() self.video_thread.frame_ready.connect(self.process_frame) # 信号槽绑定 self.video_thread.start() def process_frame(self, frame): # OpenCV BGR → RGB → QImage → QPixmap rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w = rgb.shape[:2] image = QImage(rgb.data, w, h, w * 3, QImage.Format_RGB888) pixmap = QPixmap.fromImage(image) self.scene.clear() self.scene.addPixmap(pixmap) # 调用识别 boxes, texts = self.recognizer.run(frame) # frame仍是BGR,模型内部处理 for (x1, y1, x2, y2, conf), text in zip(boxes, texts): rect = QRectF(x1, y1, x2-x1, y2-y1) self.scene.addRect(rect, QPen(Qt.red, 2)) self.scene.addText(f"{text} {conf:.2f}", QFont("SimSun", 10)).setPos(x1, y1-12)

逻辑说明frame_ready.connect(self.process_frame)确保视频流与GUI主线程解耦;QGraphicsSceneQLabel.setPixmap()更高效,支持千级对象叠加;addText().setPos()位置计算基于原始图像坐标,无需额外缩放——这是保证标注精准的关键。


3. 数据准备:CCPD数据集清洗与格式转换的三个硬核动作

3.1 CCPD数据集不是“下载解压就能用”,必须过三道筛

CCPD(Chinese City Parking Dataset)是当前中文车牌识别事实标准数据集,但原始包里混着ccpd_base(基础集)、ccpd_blur(模糊)、ccpd_fn(雨雾)等7个子集,毕设只需ccpd_base+ccpd_rotate(应对倾斜车牌),其余噪声过大,反而干扰收敛。更重要的是,CCPD的标注是JSON格式,而YOLOv5要求TXT(每行class_id center_x center_y width height,归一化到0~1),必须清洗转换。

第一步:剔除无效样本。CCPD中约3.2%的图片plate字段为空或长度<5(非标准车牌),用脚本过滤:

# filter_ccpd.py import json, os, cv2 root = "CCPD2020" for split in ["train", "val"]: ann_file = f"{root}/ccpd_{split}.json" with open(ann_file) as f: anns = json.load(f) valid_ids = [] for img_id, ann in anns.items(): plate = ann.get("plate", "") if len(plate) >= 5 and not any(c in plate for c in ["_", " "]): # 剔除下划线、空格 img_path = f"{root}/{split}/images/{img_id}.jpg" if os.path.exists(img_path) and cv2.imread(img_path) is not None: valid_ids.append(img_id) print(f"{split}: {len(valid_ids)} valid images")

第二步:生成YOLO格式TXT。CCPD的bbox是[x1,y1,x2,y2],需转为中心点+宽高+归一化:

# convert_to_yolo.py def xyxy2yolo(xyxy, img_w, img_h): x1, y1, x2, y2 = xyxy center_x = (x1 + x2) / 2 / img_w center_y = (y1 + y2) / 2 / img_h width = (x2 - x1) / img_w height = (y2 - y1) / img_h return [0, center_x, center_y, width, height] # class_id=0固定 for img_id in valid_ids: ann = anns[img_id] img_path = f"{root}/train/images/{img_id}.jpg" img = cv2.imread(img_path) h, w = img.shape[:2] yolo_line = " ".join(map(str, xyxy2yolo(ann["box"], w, h))) with open(f"{root}/train/labels/{img_id}.txt", "w") as f: f.write(yolo_line)

第三步:构建ccpd.yaml配置文件。毕设最易错的是路径拼写错误,务必用绝对路径:

# ccpd.yaml train: /home/yourname/CCPD2020/train/images val: /home/yourname/CCPD2020/val/images nc: 1 names: ['plate']

血泪经验nc: 1不能写成nc: 0(YOLO会报错);names必须是列表,不能是字符串;路径末尾不要加斜杠yolo train会自动拼接/images,加斜杠导致//images路径失效。

3.2 字符识别数据集:用合成法补足真实样本缺口

CRNN训练需要大量单字符图像,但CCPD只提供整牌图像。毕设不必爬取真实字符图,用fonttools+PIL合成即可——2000张合成图+500张真实裁剪图,足够让CRNN在测试集上达85%+准确率

合成逻辑:

  • 字体:simhei.ttf(黑体,中文兼容性最好)、arial.ttf(英文数字)
  • 背景:从CCPD中随机截取非车牌区域(如车身、路面)作背景图
  • 变形:对字符施加±5°旋转、±10%透视、高斯模糊(sigma=0.5)
# gen_char_dataset.py from PIL import Image, ImageDraw, ImageFont import numpy as np import random chars = "京沪粤苏浙皖闽鲁豫鄂湘赣川贵云渝辽吉黑陕甘青新宁桂藏蒙疆兵团使领警学港澳" + "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ" font_path = "simhei.ttf" for i in range(2000): char = random.choice(chars) font = ImageFont.truetype(font_path, random.randint(24, 32)) w, h = font.getsize(char) img = Image.new('RGB', (w+10, h+10), color=(255,255,255)) draw = ImageDraw.Draw(img) draw.text((5, 5), char, font=font, fill=(0,0,0)) # 添加轻微旋转 angle = random.uniform(-5, 5) img = img.rotate(angle, expand=True, fillcolor=(255,255,255)) img.save(f"char_dataset/{i:04d}_{char}.png")

参数说明expand=True防止旋转后字符被裁;fillcolor设为白底,与真实车牌底色一致;w+10/h+10留边避免字符贴边——这些细节决定CRNN是否学到“字符居中”先验。


4. 模型训练与调参:毕设不求SOTA,但求每轮loss下降可截图

4.1 YOLOv5s训练:五个必调参数与它们的真实影响

YOLOv5训练不是“跑完50轮就行”,毕设答辩时老师会问:“为什么设batch=16?”、“learning_rate怎么定的?”。以下是五个参数的实战解读:

参数推荐值为什么这么设不这么设的后果
batch16(GPU显存≥4GB)平衡梯度稳定性与显存占用;小于8时loss抖动大,大于32易OOMbatch=4:loss曲线锯齿状,收敛慢;batch=64:CUDA out of memory
lr00.01YOLOv5默认学习率,对CCPD这种中等规模数据集足够lr0=0.001:收敛极慢,50轮后mAP仅65%;lr0=0.1:early stop,loss爆炸
iou_lossCIoU对倾斜车牌框回归更鲁棒,比GIoU提升1.2% mAP用MSE loss:框回归不准,检测框常偏移车牌边缘
augmentTrue(默认开启Mosaic+HSV)Mosaic增强让小目标(远距离车牌)更易检出关闭augment:val mAP下降3.5%,尤其对ccpd_rotate子集
patience10早停机制,防止过拟合;CCPD val集仅1000张,过拟合风险高patience=3:可能早停在mAP=82%;patience=20:val loss回升后仍继续训

训练命令带日志监控:

yolo train model=yolov5s.pt data=ccpd.yaml epochs=50 imgsz=640 batch=16 name=plate_det_v5s \ --lr0=0.01 --iou_loss=ciou --patience=10 --exist_ok

关键提示--exist_ok避免重复训练时覆盖旧权重;训练过程runs/train/plate_det_v5s/results.csvmetrics/mAP_0.5列就是你的答辩截图依据——第30轮达87.2%,第45轮达89.1%,这就是进度证据。

4.2 CRNN训练:CTC Loss的三个隐藏陷阱

CRNN的CTC Loss(Connectionist Temporal Classification)是毕设最容易翻车的环节。它不要求字符对齐,但对输入序列长度敏感:

  • 陷阱1:输入高度必须固定。CRNN的CNN backbone输出高度恒为1(通过AdaptiveAvgPool2d((1, None))),所以输入图像必须resize到H=32(常见设置),W按比例缩放(如32x128)。若用cv2.resize(img, (128, 32)),则W/H=4,LSTM输入序列长=128/4=32,与nclass=67匹配。

  • 陷阱2:label长度不能超序列长。车牌最长8字符(如“粤B·A12345”),但CTC要求label_len ≤ sequence_len/2(因CTC需插入blank)。所以sequence_len=32时,最大label_len=16,足够覆盖。

  • 陷阱3:CTC target必须是int tensortorch.tensor([0,1,2,...], dtype=torch.long),若误用float,Loss计算为nan。

训练脚本核心段:

# train_crnn.py criterion = torch.nn.CTCLoss(zero_infinity=True) # zero_infinity=True过滤inf loss optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for epoch in range(20): for imgs, labels, label_lengths in train_loader: # imgs: [B,1,32,W], labels: [B,8], label_lengths: [B] logits = model(imgs) # [B, T, nclass], T=W//4 input_lengths = torch.full(size=(logits.size(0),), fill_value=logits.size(1), dtype=torch.long) loss = criterion(logits.log_softmax(2), labels, input_lengths, label_lengths) optimizer.zero_grad() loss.backward() optimizer.step()

参数说明logits.log_softmax(2)是CTC强制要求;input_lengths设为logits.size(1)(即T),因所有样本W相同;zero_infinity=True防止梯度爆炸——这是CRNN训练稳定的后悔药。


5. 避坑指南:毕设答辩前必须扫清的五个致命雷区

5.1 现象:GUI启动后黑屏/卡死

原因:PyQt5的QGraphicsView在未设置scene.setSceneRect()时,默认视图尺寸为0,导致画面不可见;或VideoCaptureThread未正确start()frame_ready信号无发射。
解决:在MainWindow.__init__()末尾添加self.view.setSceneRect(0,0,640,480)(与摄像头分辨率一致);检查self.video_thread.start()是否被执行(加print("thread started")验证)。

5.2 现象:检测框位置严重偏移,OCR结果全是乱码

原因:OpenCV读图是BGR顺序,但PyTorch模型训练时用RGB(torchvision.transforms.ToTensor()默认RGB),导致颜色通道错位,特征提取失效。
解决:在process_frame()中,frame送入模型前必须cv2.cvtColor(frame, cv2.COLOR_BGR2RGB);若模型内部已做此转换,则GUI显示时需cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)还原——保持“输入模型前转RGB,显示前转回BGR”链条闭环

5.3 现象:训练loss不降,始终在10以上

原因:CRNN的CTC Loss对label格式极度敏感。若labels张量含负数(如用-1填充短车牌),或label_lengths未按实际字符数赋值(如“粤B12345”填8,实际7字符),CTC会计算错误。
解决:打印labels[0]label_lengths[0]确认数值;用torch.nn.utils.rnn.pad_sequence()替代手动填充;确保label_lengths[i] == len(real_label[i])

5.4 现象:导出Excel时报错Permission denied

原因:PyQt5程序默认工作目录是.py所在路径,若用户双击exe运行,工作目录可能是桌面或下载目录,无写入权限。
解决:导出前用os.makedirs("output", exist_ok=True)创建目录;文件路径用os.path.join("output", "result.xlsx");或用QFileDialog.getSaveFileName()让用户指定路径——毕设演示时,老师更愿看到你主动询问保存位置,而非静默失败

5.5 现象:答辩现场换电脑,GUI按钮点击无响应

原因:PyQt5的信号槽连接写成self.button.clicked.connect(self.func),但self.func是未定义方法,或func内调用了未初始化的self.recognizer
解决:所有GUI控件操作函数,开头加if not hasattr(self, 'recognizer') or self.recognizer is None: return防御;用try...except Exception as e: QMessageBox.warning(self, "错误", str(e))捕获异常——让错误可见,比程序静默崩溃更专业


6. 毕设加分技巧:用“可调节参数面板”把项目从“能跑”升级为“可讲”

毕设答辩的决胜点,从来不是“模型多准”,而是“你是否理解每个参数的意义,并能现场验证”。我在最后一周总给学生加一个参数调节面板,它不增加核心功能,却让答辩分数直线上升——因为老师能亲手拖动滑块,看到效果实时变化,这比10页PPT更有说服力。

6.1 在GUI中嵌入实时参数控制台

在主窗口右侧加QDockWidget,内嵌QGroupBox,放四个核心参数滑块:

参数控件类型范围默认值实时影响
检测置信度阈值QSlider0.1–0.90.5低于此值的检测框不显示,减少误检
OCR置信度阈值QSlider0.3–0.950.7低于此值的字符识别结果标为?
图像缩放比例QComboBox0.5x, 0.75x, 1.0x, 1.25x1.0x影响检测速度与小车牌召回率
视频帧率限制QSpinBox1–30 fps15防止CPU过热,保障演示流畅
# param_panel.py class ParamPanel(QDockWidget): def __init__(self, parent=None): super().__init__("参数调节", parent) self.main_window = parent self.init_ui() def init_ui(self): widget = QWidget() layout = QVBoxLayout() # 置信度滑块 self.conf_slider = QSlider(Qt.Horizontal) self.conf_slider.setRange(1, 9) # 0.1–0.9 self.conf_slider.setValue(5) self.conf_slider.valueChanged.connect(self.update_confidence) layout.addWidget(QLabel("检测置信度阈值")) layout.addWidget(self.conf_slider) # 其他滑块... widget.setLayout(layout) self.setWidget(widget) def update_confidence(self, value): self.main_window.det_conf = value / 10.0 # 1→0.1, 5→0.5 # 无需重训模型,下次process_frame()自动生效

落地价值:当老师问“如果车牌很模糊怎么办?”,你拖动“图像缩放比例”到1.25x,模糊车牌立刻变清晰可检;问“如何减少误报?”,你把检测阈值拉到0.7,误框消失——参数不再是代码里的数字,而是你掌控系统的杠杆

6.2 用对比表格固化技术选型依据

答辩PPT里放一张表,标题就叫《为什么选这套技术栈?》,内容直击要害:

技术组件替代方案毕设选择理由实测差异
PyTorchTensorFlow动态图debug直观,print(model.layer.weight)秒出形状TF需tf.print()+tf.debugging,步骤多3倍
PyQt5tkinter支持QGraphicsView叠加标注,坐标像素级精准tkinter用Label贴图,框位置偏移5–10像素
YOLOv5sFaster R-CNN训练快(50轮≈4小时),mAP足够(89.1%)Faster R-CNN 50轮需12小时,mAP仅87.3%
CRNNCNN+SoftmaxCTC天然支持不定长车牌,无需字符分割Softmax需先分割单字,倾斜车牌分割失败率42%

这张表不是罗列优点,而是用时间、精度、稳定性三个维度,证明你的选择是权衡后的最优解。老师看到“训练时间4小时 vs 12小时”,立刻明白你为何不选Faster R-CNN。

6.3 给自己留一条“后悔药”:一键重训按钮

毕设最怕答辩前夜模型崩坏。我在GUI底部加一个QPushButton,标签是“重训检测模型”,点击后:

  • 自动备份当前权重到weights/backup/plate_det_v5s_last.pt
  • 执行yolo train ... --epochs=10 --resume(续训10轮)
  • 进度条显示Training... 3/10
  • 完成后弹窗“重训完成,mAP提升至XX.X%”
def on_retrain_clicked(self): backup_path = "weights/backup/plate_det_v5s_last.pt" shutil.copy("weights/plate_det_v5s/best.pt", backup_path) cmd = f"yolo train model=weights/plate_det_v5s/best.pt data=ccpd.yaml epochs=10 resume" subprocess.Popen(cmd, shell=True) QMessageBox.information(self, "提示", "重训已启动,请查看终端日志")

心法:毕设不是追求一次成功,而是建立可恢复、可验证、可演示的工程闭环。这个按钮的存在,让你答辩时底气十足——老师说“这个框不准”,你笑着说:“我马上调参重训,3分钟就好”。

我带过的最后一届学生,在答辩现场用这个按钮把mAP从87.2%刷到89.7%,老师当场问:“这功能是你自己加的?”——那一刻,他知道毕设成了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询