YOLOv5+LPRNet轻量车牌识别项目全流程拆解
简介这份基于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-154383-386473-386473-391154391-106_106_27_29_64_28_57-109-106.jpg拆开来看每个由连字符分隔的字段含义大致如下分段内容含义025025亮度特征不同版本的 CCPD 对该段解读略有差异95_11395_113车牌面积与宽高比相关特征154383-386473-386473-391154391bbox 四角坐标车牌的四个角点坐标按顺时针排列106_106_27_29_64_28_57字符编码车牌字符对应的分类索引逐字符109_106109_106车牌区域的亮度和对比度特征转 YOLO 格式最关键的是第三段。四个角点分别是左上、右上、右下、左下格式是x1y1-x2y2-x3y3-x4y4。我们在转换时取这四点的最小外接矩形得到的就是车牌检测框。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(-) # 第三段是角点坐标格式 x1y1-x2y2-x3y3-x4y4 corners parts[2].split(-) pts [] for corner in corners: x, y corner.split() pts.append([int(x), int(y)]) pts np.array(pts, dtypenp.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(f0 {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.1LPRNet 的数据准备和 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.yamlYOLOv5 的训练流程大家应该不陌生但有几个配置文件需要根据车牌场景调整。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超参数里我特别想提两个。第一个是degreesYOLOv5 默认是 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 是不是持续下降第二个是验证集的 mAP0.5 有没有超过 90。正常情况下在 CCPD 上训练 100 个 epochmAP0.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 LossPyTorch 里直接调用torch.nn.CTCLoss即可。数据加载器需要额外生成两个东西input_lengths和target_lengths。后者是每个车牌真实字符长度中国车牌是 7 位或 8 位新能源车牌多一位训练时要按批次内最长的目标做 paddingCTCLoss 会根据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(dim2) loss F.ctc_loss( log_probs, labels, input_lengths, target_lengths, blank0, reductionmean ) optimizer.zero_grad() loss.backward() # 梯度裁剪防止 CTC 训练早期梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm3.0) optimizer.step() return loss.item()这里有个参数容易踩坑blank0是指 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_result和rec_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_thres0.3, iou_thres0.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), interpolationcv2.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 附近就不再下降mAP0.5 只有 85。原因标签文件里大概率有坐标越界或者 class id 错误。解决写脚本把标注画回图上看检查标签文件里的width和height是不是都小于 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/做车牌识别项目最容易犯的错是检测和识别各自准确率都很高就以为整个系统没问题。实际上从检测框到裁剪、从裁剪到缩放、从缩放到灰度化每个环节都会把误差放大一点。从那以后我每次做完训练都会在真实场景的图片集上强制走一遍完整流程保存那些识别失败的图逐个看是哪个环节断的再决定去调哪个模型。希望帮到你。本文还有配套的精品资源点击获取