YOLOv5+LPRNet轻量车牌识别项目全流程拆解
2026/9/24 21:03:29 网站建设 项目流程

简介:这份基于YOLOv5与LPRNet的车牌检测识别项目,面向计算机、电子信息工程、数学等专业学生,适用于课程设计、期末大作业或毕业设计参考。项目以CCPD数据集为基础,YOLOv5s负责车牌定位,LPRNet完成字符识别,当前支持蓝牌和新能源绿牌,并保留后续微调扩展能力。压缩包共61个文件,包含27张图像样例、22个Python脚本、3个YAML配置以及预训练权重(pth/pt)、pptx/docx/md说明文档等,整体大小16.75MB,目录按数据转换、训练、检测、工具函数、权重等模块划分,结构清晰易检索。代码覆盖CCPD数据转YOLO/LPR格式、数据集划分、YOLOv5训练、LPRNet训练、独立检测与端到端测试完整流程,并配有演示图片、检测结果和训练曲线,便于对照验证。目前已有482人学习下载,适合希望快速上手车牌识别全链路、参考轻量级工程实现的中级开发者。

1. 为什么是“YOLOv5 检测 + LPRNet 识别”:轻量车牌识别项目拆解

汽车牌照识别这种需求,真正的难点不在模型有多深,而在两个环节各干各的活:第一步得把车牌从复杂背景里找出来,第二步得把牌照上的字符读准。用 YOLOv5 做检测、LPRNet 做识别,是一个已经被验证过的成熟搭配,而且不用上 GPU 集群,一张消费级显卡就能把训练跑完,推理阶段用 CPU 也能勉强动起来。这个开源项目就是按这个思路来的,把 CCPD 数据集从原始图片一路处理成 YOLO 格式和 LPRNet 格式,分别训练检测模型和识别模型,最后串成一个完整的端到端流程。目前支持蓝牌和新能源绿牌,适合做着玩票、做课程设计或者快速搭一个车牌识别 demo 的人,也适合想搞清楚检测和识别两个模型到底怎么衔接的从业者。

你会拿到的东西包括:完整的 CCPD 转 YOLO 训练格式脚本、CCPD 转 LPRNet 训练格式脚本、YOLOv5s 和 LPRNet 的训练脚本、两个训练好的权重文件(yolov5_best.pt 和 lprnet_best.pth),以及一个把检测和识别串起来的 main.py。也就是说,视觉的、检测发型的、做 OCR 的,都能在这个项目里找到自己能改的部分。

2. 数据准备:CCPD 转 YOLO 格式与 LPRNet 格式

2.1 先搞懂 CCPD 文件名:坐标、车牌号全在里面

CCPD 数据集的全称是 Chinese City Parking Dataset,图片基本都来自停车场场景,车辆角度、光照、模糊程度都挺复杂。它最大的特点就是所有标注信息都编码在文件名里,不需要额外的 XML 或者 JSON 标注文件。这个设计在数据读取时非常高效,但对第一次接触的人来说是个门槛,因为文件名看过去就是一堆数字和下划线。

拿一张典型的 CCPD 图片文件名来说:

025-95_113-154&383-386&473-386&473-391&154&391-106_106_27_29_64_28_57-109-106.jpg

拆开来看,每个由连字符分隔的字段含义大致如下:

分段内容含义
025025亮度特征(不同版本的 CCPD 对该段解读略有差异)
95_11395_113车牌面积与宽高比相关特征
154&383-386&473-386&473-391&154&391bbox 四角坐标车牌的四个角点坐标,按顺时针排列
106_106_27_29_64_28_57字符编码车牌字符对应的分类索引,逐字符
109_106109_106车牌区域的亮度和对比度特征

转 YOLO 格式最关键的是第三段。四个角点分别是左上、右上、右下、左下,格式是x1&y1-x2&y2-x3&y3-x4&y4。我们在转换时取这四点的最小外接矩形,得到的就是车牌检测框。

2.2 ccpd2yolov5.py 转换逻辑:从角点到 YOLO 标签

YOLO 训练需要的标签格式是归一化后的中心点坐标和宽高,也就是class_id center_x center_y width height,所有数值除以图像宽高,范围在 0 到 1 之间。ccpd2yolov5.py 的核心工作就是把文件名里的角点坐标换算成这种格式。

import os import cv2 import numpy as np def parse_ccpd_name(filename): # 去掉扩展名,按 - 分段 parts = filename.split('.')[0].split('-') # 第三段是角点坐标,格式 x1&y1-x2&y2-x3&y3-x4&y4 corners = parts[2].split('-') pts = [] for corner in corners: x, y = corner.split('&') pts.append([int(x), int(y)]) pts = np.array(pts, dtype=np.float32) # 四角点 # 第四段是字符编码,后续 LPRNet 数据准备会用到 char_codes = parts[3].split('_') return pts, char_codes def convert_to_yolo_label(img_path, label_path): img = cv2.imread(img_path) h, w = img.shape[:2] pts, _ = parse_ccpd_name(os.path.basename(img_path)) x_min = max(0, int(pts[:, 0].min())) y_min = max(0, int(pts[:, 1].min())) x_max = min(w, int(pts[:, 0].max())) y_max = min(h, int(pts[:, 1].max())) # YOLO 格式:归一化中心坐标和宽高 cx = ((x_min + x_max) / 2) / w cy = ((y_min + y_max) / 2) / h bw = (x_max - x_min) / w bh = (y_max - y_min) / h # 单类别车牌检测,class id 固定为 0 with open(label_path, 'w') as f: f.write(f'0 {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n')

这段代码的逻辑不复杂,但有两个细节值得留意。第一,取角点坐标后一定要做越界裁剪,max(0, min())这步不能省,因为 CCPD 里少量图片的车牌角点会超出图像边界,如果不裁剪,训练时 YOLO 会算出大于 1 或小于 0 的坐标,轻则警告,重则 loss 变成 NaN。第二,坐标系别搞混,OpenCV 读取的图是 BGR,坐标系本身没影响,但如果后续你做了翻转增强,坐标也要跟着变,这个脚本里没有做增强,所以不用管。

2.3 split_dataset.py 和 LPRNet 数据准备

数据和标签准备好之后,要按训练集、验证集、测试集切分。项目里 split_dataset.py 做的事情就是随机划分目录,我一般习惯按 8:1:1 来分,如果你数据量特别大,比如用全套 CCPD,那 9:0.5:0.5 也问题不大。

python split_dataset.py \ --image_dir /data/CCPD/images \ --label_dir /data/CCPD/labels \ --output_dir /data/CCPD/split \ --train_ratio 0.8 \ --val_ratio 0.1 \ --test_ratio 0.1

LPRNet 的数据准备和 YOLO 不一样,它需要的是车牌图,不是整张停车场图。所以 ccpd2lpr.py 做的事情是:根据角点坐标把车牌区域裁出来,缩放到 LPRNet 需要的输入尺寸,然后从文件名里提取字符编码存成标签。这里有个常见做法,裁车牌时建议往外扩 5 到 10 个像素,不要裁得太紧,否则边缘字符容易被截断,识别精度会掉得莫名其妙。

LPRNet 的输入尺寸是宽 94 像素、高 24 像素,灰度图。字符集要单独维护,中国车牌第一个字符是省份简称汉字,后面是字母和数字的组合。CCPD 数据集里字符类别固定 68 类,包含 31 个省份汉字、数字 0-9、字母 A-Z(去掉 I 和 O)。字符索引表要全局统一,训练和推理用同一个映射文件,不然会遇到模型输出和预期完全对不上的问题。

3. 训练检测器:YOLOv5s 与超参数设置

3.1 数据配置文件:ccpd.yaml 和 hyp.scratch.yaml

YOLOv5 的训练流程大家应该不陌生,但有几个配置文件需要根据车牌场景调整。data/ccpd.yaml 定义了数据路径和类别信息,单类别检测所以 nc 是 1,类别名是 license_plate。hyp.scratch.yaml 是超参数配置,项目里给的版本针对 CCPD 做了取舍,主要改动在锚框大小、学习率和数据增强强度上。

# ccpd.yaml train: /data/CCPD/split/train/images val: /data/CCPD/split/val/images test: /data/CCPD/split/test/images nc: 1 names: ['license_plate']
# hyp.scratch.yaml 关键参数 lr0: 0.01 # 初始学习率 lrf: 0.2 # 最终学习率 = lr0 * lrf momentum: 0.937 weight_decay: 0.0005 warmup_epochs: 3.0 box: 0.05 # box loss 系数 cls: 0.5 # 分类 loss 系数 obj: 1.0 # 目标置信度 loss 系数 hsv_h: 0.015 # HSV 色调增强 hsv_s: 0.7 # 饱和度增强 hsv_v: 0.4 # 明度增强 degrees: 5.0 # 旋转增强,车牌不适用大角度 translate: 0.1 scale: 0.5 fliplr: 0.5

超参数里我特别想提两个。第一个是degrees,YOLOv5 默认是 0.0,如果不改,模型对倾斜车牌的鲁棒性会差;调成 5 度到 10 度之间比较合适,但别调大,车牌本身的旋转范围就在这个区间,太大反而会让模型学到失真形态。第二个是hsv_h,车牌的颜色是蓝和绿,色调增强太猛会把蓝牌变成紫牌、绿牌变成黄牌,这是很多人忽略的坑。

3.2 训练 YOLOv5s:命令、显存和训练节奏

模型结构用的是 yolov5s.yaml,这是 YOLOv5 系列里最小的一个版本,约 700 万参数。车牌检测本身是个单目标任务,不需要用 m 或者 l 版本,s 足够,而且推理速度快,CPU 也能跑得动。训练命令和 YOLOv5 官方几乎一致:

python train_yolov5.py \ --data data/ccpd.yaml \ --cfg models/yolov5s.yaml \ --hyp data/hyp.scratch.yaml \ --weights weights/yolov5s.pt \ --img-size 640 \ --batch-size 32 \ --epochs 100 \ --device 0

几个参数根据自己的实际情况调。--img-size是输入分辨率,640 是默认值,CCPD 里车牌的像素宽度一般在 80 到 150 之间,640 的分辨率足够模型感受到车牌细节。--batch-size取决于显存,8GB 显存跑 640 分辨率、32 的 batch 会有点紧,可以先降一半试跑一个 epoch 再看显存占用。--weights用 COCO 预训练权重做迁移学习,而不是从零开始,检测框的收敛速度快很多,训练到第 10 个 epoch 左右框就开始准了。

训练过程中要盯几个关键指标,第一个是 box_loss 和 obj_loss 是不是持续下降,第二个是验证集的 mAP@0.5 有没有超过 90。正常情况下,在 CCPD 上训练 100 个 epoch,mAP@0.5 应该到 98 以上,因为 CCPD 虽然场景多样,但标注质量高、目标单一,检测本身没什么难度。如果 mAP 卡在 80 上不来,大概率是数据转换出了问题,回去看看标签画的框对不对,而不是怀疑模型。

3.3 检测器训练的边界:什么情况算够了

车牌检测这个任务有一个特殊性——它跟通用目标检测不一样,一个场景里通常只有一块车牌,漏检和误检的代价都很高。我在训练时发现,YOLOv5s 在 CCPD 上很容易过拟合,表现为训练集 mAP 接近 100,但验证集掉到 90 以下。解决办法不是换大模型,而是加验证集数据、调高cls系数,或者干脆提前停止,选验证集 mAP 最高的那个权重而不是最后一个 epoch 的权重。

另一个需要注意的是置信度阈值的选择。训练好之后做推理,threshold 设在 0.25 或者 0.3 比较合理,CCPD 里的车牌在画面里通常占比不小,模型给出的置信度往往在 0.8 以上。如果看到一个框上贴了好几个候选框,那就是 NMS 的 IOU 阈值要调小,默认 0.45 是通用目标检测的经验值,车牌这种单一目标场景可以调到 0.5 以上,减少相邻框被误杀的情况。

4. 训练识别器:LPRNet 与 CTC 损失

4.1 LPRNet 网络结构:序列识别怎么解出车牌号

LPRNet 的设计思路和 CRNN 类似,核心是 CNN 提取特征 + 循环网络捕捉序列关系 + CTC 损失对齐。它不需要先把车牌切成单个字符,而是把整张车牌当成一个序列去识别,省掉了字符分割这一步,这对中文车牌这种字符间距不均匀的情况特别友好。

实际结构来说,LPRNet 先用一系列卷积层把输入图压缩成特征序列,然后过一个双向 LSTM 建模字符之间的上下文关系,最后通过一个全连接层输出每个时间步的字符概率分布。车牌宽度是 94 像素,经过网络下采样后序列长度会缩短,最终得到约 24 个时间步,每个时间步输出 68 个类别的概率。CTC 负责把时间步序列对齐到最终的车牌字符串,同时允许空白符存在,这样模型不需要精确知道每个字符的边界在哪。

4.2 train_lprnet.py 训练流程与关键参数

训练 LPRNet 用的损失函数是 CTC Loss,PyTorch 里直接调用torch.nn.CTCLoss即可。数据加载器需要额外生成两个东西:input_lengthstarget_lengths。后者是每个车牌真实字符长度,中国车牌是 7 位或 8 位(新能源车牌多一位),训练时要按批次内最长的目标做 padding,CTCLoss 会根据target_lengths自动忽略 padding 部分。

import torch import torch.nn.functional as F # 每批次训练核心逻辑 def train_one_step(model, images, labels, input_lengths, target_lengths): model.train() logits = model(images) # [N, 68, T],T 为时间步数 log_probs = logits.log_softmax(dim=2) loss = F.ctc_loss( log_probs, labels, input_lengths, target_lengths, blank=0, reduction='mean' ) optimizer.zero_grad() loss.backward() # 梯度裁剪防止 CTC 训练早期梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=3.0) optimizer.step() return loss.item()

这里有个参数容易踩坑,blank=0是指 CTC 空白类的索引为 0,字符索引要从 1 开始编号,比如 0 表示空白,1-31 是省份汉字,32 开始是数字和字母。如果字符索引从 0 开始,CTC 会把第一个字符当成空白符,辨识率直接崩盘。input_lengths通常设为模型输出的时间步数,比如 24,但要确认数据加载时没有因为 batch 内图像宽度不一致而做缩放,LPRNet 的输入固定 94×24,所以这个参数在项目里是常量。

训练超参数方面,学习率初始 0.001,采用 ReduceLROnPlateau 策略,验证集 loss 下降平缓时降为原来的 0.1。车牌识别任务没有预训练权重可用,必须从零开始训练,所以前期收敛速度比 YOLO 慢,一般 50 个 epoch 后准确率才会稳定,100 个 epoch 打底。

4.3 test_lprnet.py 验证:不要只盯准确率一个数

LPRNet 训练完成后,项目里给了 test_lprnet.py 做验证。验证指标除了整体识别准确率之外,我建议额外算两个:字符级准确率和难例召回率。字符级准确率的意义在于,即使整张车牌识别错了,如果 7 个字符里有 6 个是对的,剩下那个错的往往是因为边缘字符被裁剪掉或者模糊,这能帮你定位是检测环节的问题还是识别环节的问题。

跑验证脚本时,把det_resultrec_result两个目录打开看看,里面存的是中间结果图。这一步值得花时间,因为看保存的图片比看 loss 曲线直观得多。如果裁剪出来的车牌图片本身是歪的、缺角的,那问题在检测框;如果图是正的但识别结果错了,那问题在 LPRNet 的训练数据或模型容量上。

5. 端到端测试与常见问题排查:三步定位识别失败的环节

5.1 分模块测试:detect_yolov5.py 和 test_lprnet.py

端到端流程出问题时,最忌讳直接去改模型参数。正确的做法是先分模块验证,把问题限定在检测、裁剪、识别三段之一。项目里的 detect_yolov5.py 会读取一张测试图,输出检测框并在图上画出来,保存到 det_result 目录。

# 先只测检测模块 python detect_yolov5.py \ --weights weights/yolov5_best.pt \ --source data/images/sample.jpg \ --conf-thres 0.3 \ --iou-thres 0.5 \ --output det_result/

如果检测框没有正确框住车牌,后面的识别再准也没用。检测框常见的问题有三个:框住但偏大、框住但偏小、漏检。偏大和偏小都会让 LPRNet 的输入里混入过多背景或切掉字符边缘,识别率直接下降;漏检则是另一个层面的问题,需要回看训练数据的覆盖度。

识别模块单独测试用的是 test_lprnet.py,输入是裁剪好的车牌图,输出是车牌字符串。这一步要注意,LPRNet 的输入是灰度图,测试脚本里要确保cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)这一步存在,很多从 YOLO 流程直接搬过来的代码容易漏掉灰度转换,导致通道数不匹配直接报错。

5.2 用 main.py 串起整个流程

main.py 的作用是把检测和识别串到一起,逻辑上没什么花头,但边界条件处理是重点。核心流程如下:

import cv2 import torch import numpy as np def plate_recognition(image, detector, lprnet, char_dict): # 1. 检测阶段 results = detector(image) # YOLOv5 前向传播 boxes = filter_boxes(results, conf_thres=0.3, iou_thres=0.5) plates = [] for box in boxes: x1, y1, x2, y2 = box # 2. 裁剪车牌,向外扩展 8% 防止边缘字符被切 margin_w = int((x2 - x1) * 0.08) margin_h = int((y2 - y1) * 0.08) x1 = max(0, x1 - margin_w) y1 = max(0, y1 - margin_h) x2 = min(image.shape[1], x2 + margin_w) y2 = min(image.shape[0], y2 + margin_h) crop = image[y1:y2, x1:x2] # 3. 识别阶段 gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) resized = cv2.resize(gray, (94, 24), interpolation=cv2.INTER_CUBIC) tensor = torch.from_numpy(resized).unsqueeze(0).unsqueeze(0).float() / 255.0 preds = lprnet(tensor) plate_text = decode_predictions(preds, char_dict) plates.append(plate_text) return plates

这段代码里我特意在裁剪时加了 margin 外扩,这是血泪经验。YOLO 检测框是按最小外接矩形算的,很多情况下车牌边缘字符本来就贴边,裁得跟框一样齐,第一个字符和最后一个字符就残了。外扩 8% 能明显提升识别率。

5.3 常见问题与避坑记录:四条高频踩坑

现象一:识别结果里字符缺头缺尾,比如“京A12345”识别成“A1234”。原因:检测框裁剪时没有 margin,边缘字符被切掉。解决:裁剪时向外扩 5 到 10 像素,并用max(0, ...)min(w/h, ...)做边界钳制,防止越界。

现象二:LPRNet 在验证集上准确率高,但部署到新的停车场图片上识别率掉一半。原因:CCPD 数据主要来自安徽合肥的停车场,场景单一,模型过拟合了特定场地特征。解决:收集目标场景的车牌数据做微调,1000 张左右就能有明显效果;如果不想收集数据,把测试集的图片做随机亮度和对比度扰动,先看掉点幅度评估一下模型稳健性。

现象三:训练 YOLO 的时候 loss 降到 0.05 附近就不再下降,mAP@0.5 只有 85。原因:标签文件里大概率有坐标越界或者 class id 错误。解决:写脚本把标注画回图上看,检查标签文件里的widthheight是不是都小于 1,以及图像和标签的对应关系是否错位。这个翻车我遇到过两次,每次都是数据问题不是模型问题。

现象四:端到端推理速度只有 8 FPS,感觉达不到实时。原因:训练好的模型默认全精度推理,且没有关闭梯度跟踪。解决:推理时下torch.no_grad()上下文,模型切换到model.eval()模式,如果显存或内存允许,再用model.half()转半精度推理,速度能提升 30% 以上。

6. 提升精度的实用技巧:微调策略与只跑一次的验证脚本

模型训练到一个理想状态后,如果想进一步压榨效果,有几个不用重新训练就能见效的操作。第一,调整检测置信度阈值。YOLO 默认置信度 0.25,在车牌场景下可以尝试拉高到 0.4 甚至 0.5,因为 CCPD 训练出的模型对车牌的置信度普遍在 0.9 以上,拉高阈值能滤掉那些低置信度的误检框,降低 LPRNet 被喂入垃圾输入的概率。第二,LPRNet 推理时做三个尺度的投票,把车牌图分别缩放到 94×24、100×26、88×22 送入模型,三个结果取多数投票。这种方式能让准确率提高 0.5 到 1 个百分点,代价是推理时间翻三倍,适合自己的场景里决定取舍。

验证脚本值得花时间写完整,我会在每次改完参数后强制跑一遍同样的测试集,输出三个指标:检测框 IoU 均值、字符级准确率、整牌准确率。不要加新的图片,不要换测试集,只改参数,保证横向对比有意义。写这个脚本的功力会直接影响你对模型状态的判断,没有量化就不能优化。

# 一条命令跑完检测 + 识别 + 指标统计 python evaluate_pipeline.py \ --det-weights weights/yolov5_best.pt \ --lpr-weights weights/lprnet_best.pth \ --test-dir /data/CCPD/split/test \ --conf-thres 0.3 \ --iou-thres 0.5 \ --save-error-cases error_cases/

做车牌识别项目最容易犯的错,是检测和识别各自准确率都很高就以为整个系统没问题。实际上从检测框到裁剪、从裁剪到缩放、从缩放到灰度化,每个环节都会把误差放大一点。从那以后我每次做完训练,都会在真实场景的图片集上强制走一遍完整流程,保存那些识别失败的图,逐个看是哪个环节断的,再决定去调哪个模型。希望帮到你。

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

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

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

立即咨询