☰
YOLOv5+LPRNet车牌识别实战:CCPD数据集训练与避坑指南
2026/10/2 14:06:14 网站建设 项目流程

简介:这份资源面向计算机视觉入门与进阶开发者,提供一套基于YOLOv5与LPRNet的车牌检测与识别完整实现,解决从图像中定位车牌并识别字符的工程问题,适用于交通监控、停车管理等场景的学习与二次开发。压缩包共59个文件,约16.73MB,包含22个Python脚本(训练、测试、数据转换与推理入口)、27张jpg示例图、3个yaml配置、3张png结果图,以及lprnet_best.pth与yolov5_best.pt两个预训练权重和说明文档,覆盖数据准备到模型部署的主要环节。已有816人学习下载。资源以CCPD数据集为训练与测试来源,代码中提供ccpd2yolov5、ccpd2lpr等转换脚本,并附带检测与识别结果图,便于读者快速复现YOLOv5检测加LPRNet识别的两阶段流程,理解边界框输出、车牌裁剪预处理与字符识别网络的衔接方式,同时可参考权重文件直接验证效果,适合作为课程设计或项目原型的起步模板。

1. 从一张停车场抓拍图说起:YOLOv5+LPRNet 车牌检测识别到底怎么落地

前阵子帮朋友处理一批停车场抓拍图,两千多张,角度歪、光照乱、车牌还带各种蓝绿渐变。他原本想用传统 OpenCV 轮廓法硬怼,结果一半以上车牌定位都飘。后来换成 YOLOv5 做检测、LPRNet 做识别这套组合,才把整条链路跑通。这套方案的核心思路很清晰:YOLOv5 负责在整张图里框出车牌位置,LPRNet 负责把裁出来的车牌小图转成字符序列。两者解耦,各自训练、各自优化,比端到端模型好调试得多。CCPD 数据集是国内车牌场景里绕不开的公开数据源,标注格式和真实抓拍接近,适合拿来练手或做基线。如果你手头有车牌识别需求,又不想从零造轮子,这套组合值得花时间拆一遍。下面按「数据怎么准备 → 模型怎么训 → 坑在哪 → 怎么验证」的顺序,把能复现的细节都摊开。

2. 数据管线:CCPD 数据集解析与 YOLO 格式转换

2.1 CCPD 的目录结构与标注逻辑

CCPD 数据集的文件名本身就是标注,这一点和很多数据集不同。以025-95_113-226&469_448&520-444&522_454&542_454&542_454&542-0_0_22_27_27_33_16-66-88.jpg为例,按-切分后各字段含义如下:

字段序号含义示例值
0区域编号025
1倾斜角度95_113
2边界框坐标226&469_448&520
3四角点坐标444&522_454&542_...
4车牌字符索引0_0_22_27_27_33_16
5亮度66
6模糊度88

其中字段 2 的226&469_448&520表示左上角(226,469)、右下角(448,520),这就是 YOLO 训练需要的检测框。字段 4 的七个数字对应车牌字符在省份、字母、数字字典里的索引,LPRNet 训练时要用。

常见做法是先把 CCPD 的标注转成 YOLO 的 txt 格式,每行class x_center y_center width height,坐标归一化到 0~1。转换脚本我一般这么写:

import os import cv2 # CCPD 字符字典,按实际数据集版本调整 provinces = ["皖", "沪", "津", "渝", "冀", "晋", "蒙", "辽", "吉", "黑", "苏", "浙", "京", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "桂", "琼", "川", "贵", "云", "藏", "陕", "甘", "青", "宁", "新"] alphabets = ['A','B','C','D','E','F','G','H','J','K','L','M','N','P','Q','R','S','T','U','V','W','X','Y','Z'] ads = ['0','1','2','3','4','5','6','7','8','9'] def ccpd_to_yolo(img_dir, label_dir): os.makedirs(label_dir, exist_ok=True) for fname in os.listdir(img_dir): if not fname.endswith('.jpg'): continue parts = fname.split('-') if len(parts) < 7: continue # 解析边界框 bbox = parts[2].split('_') x1, y1 = map(int, bbox[0].split('&')) x2, y2 = map(int, bbox[1].split('&')) img_path = os.path.join(img_dir, fname) img = cv2.imread(img_path) if img is None: continue h, w = img.shape[:2] # 归一化并转为中心点+宽高 xc = (x1 + x2) / 2.0 / w yc = (y1 + y2) / 2.0 / h bw = (x2 - x1) / w bh = (y2 - y1) / h # 类别 0 表示车牌 line = f"0 {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}\n" txt_name = os.path.splitext(fname)[0] + '.txt' with open(os.path.join(label_dir, txt_name), 'w') as f: f.write(line) if __name__ == '__main__': ccpd_to_yolo('./ccpd/images', './ccpd/labels')

这段脚本的关键点有三个:一是parts[2]的解析必须严格按&和_切,CCPD 不同子集(base、blur、tilt 等)字段顺序一致,但个别文件名可能缺字段,所以加了len(parts) < 7的过滤;二是归一化用的是原图宽高,不是 640×640,YOLOv5 训练时会自己 resize;三是类别统一写 0,因为只检测车牌一类。跑完以后建议抽查几张,用labelImg或自己写个可视化脚本叠框看一眼,确认没有坐标翻转。

2.2 训练集/验证集划分与 LPRNet 标签生成

YOLO 那边只需要图片和 txt,LPRNet 这边需要裁出来的车牌小图和对应字符标签。我一般先用训练好的 YOLO 权重跑一遍检测,把车牌区域裁出来存成rec_images/,同时生成rec_labels.txt,每行图片名 字符序列。字符序列的生成逻辑是:把字段 4 的七个索引分别映射到省份、字母、数字字典,拼成字符串。注意 CCPD 里字母字典没有I和O,和国内车牌规则一致,映射时别用错字典。

划分比例上,CCPD 官方建议按 8:1:1 分训练、验证、测试。但实际做的时候,如果只做检测,YOLO 的验证集可以小一点;如果检测和识别一起调,识别模型的验证集要单独留,不能和检测混用,否则评估结果会虚高。我通常固定随机种子,按文件名哈希取模来分,保证每次复现划分一致。

提示:CCPD 里有些图片车牌区域极小或严重模糊,转换后建议按框面积过滤掉宽或高小于 20 像素的样本,否则 YOLO 训练时这些小目标会拉低召回。

3. YOLOv5 车牌检测:环境配置、训练参数与推理后处理

3.1 环境配置与 conda 隔离

YOLOv5 对环境比较敏感,尤其是 PyTorch 和 CUDA 版本。我习惯用 conda 建独立环境,避免和系统里的其他项目打架:

conda create -n yolov5-lpr python=3.8 -y conda activate yolov5-lpr # 按自己显卡驱动选对应 CUDA 版本的 PyTorch pip install torch==1.12.1+cu113 torchvision==0.13.1+cu113 -f https://download.pytorch.org/whl/torch_stable.html git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt

装完以后跑python detect.py --source data/images --weights yolov5s.pt验证环境。如果报CUDA out of memory,先把--device cpu加上确认代码没问题,再回头查显卡占用。常见坑是 conda 环境里装了 CPU 版 PyTorch,torch.cuda.is_available()返回 False,这时候要卸掉重装对应 CUDA 版本。

3.2 训练参数怎么设:从 yolov5s 起步

车牌检测属于单类目标检测,数据量几千到几万张,用yolov5s就够,没必要上 l 或 x。我一般这么起训:

python train.py \ --img 640 \ --batch 16 \ --epochs 100 \ --data ccpd.yaml \ --weights yolov5s.pt \ --cfg models/yolov5s.yaml \ --hyp data/hyps/hyp.scratch-low.yaml \ --name ccpd_yolov5s

ccpd.yaml里写清楚train、val路径和nc: 1、names: ['plate']。--img 640是默认值,如果车牌在图中占比很小,可以提到 1280,但显存翻倍,batch 要相应降到 8 或 4。--hyp用 low 增强版本,因为车牌颜色和纹理变化大,Mosaic、HSV 增强能明显提升泛化。训练过程中重点看mAP@0.5和mAP@0.5:0.95,车牌检测一般 mAP@0.5 能到 0.95 以上,如果卡在 0.8 左右,多半是标注框有偏移或漏标。

3.3 推理与后处理:NMS 阈值和裁剪边距

训练完拿best.pt推理,YOLOv5 的detect.py默认 NMS IoU 阈值 0.45、置信度 0.25。车牌场景下,如果一张图里只有一块车牌,这两个值不用大改;如果有多车牌或误检多,把置信度提到 0.5、NMS 降到 0.4 试试。裁车牌时别贴着框裁,四周留 5~10 像素边距,LPRNet 对边缘敏感,裁太紧会把字符切掉。

import torch from PIL import Image model = torch.hub.load('ultralytics/yolov5', 'custom', path='best.pt') img = Image.open('test.jpg') results = model(img) # 取第一个检测框,留边距裁剪 for *box, conf, cls in results.xyxy[0].tolist(): x1, y1, x2, y2 = map(int, box) pad = 8 crop = img.crop((max(0, x1-pad), max(0, y1-pad), min(img.width, x2+pad), min(img.height, y2+pad))) crop.save('plate_crop.jpg')

这段代码里results.xyxy[0]是检测结果张量,每行x1,y1,x2,y2,conf,cls。pad设 8 是经验值,CCPD 里车牌框通常比较准,留太多会引入背景干扰。裁完的图统一 resize 到 LPRNet 需要的尺寸,一般是 94×24,保持宽高比做 padding,别直接拉伸。

4. LPRNet 识别:字符字典、CTC 损失与训练细节

4.1 LPRNet 结构为什么适合车牌

LPRNet 全称 License Plate Recognition Network,核心是不用 RNN、不用分割,直接 CNN + CTC 输出字符序列。车牌字符长度固定(国内蓝牌 7 位,新能源 8 位),但 CTC 允许输入不定长,所以同一套网络能兼容不同长度车牌。它的 backbone 是轻量 CNN,参数量小,推理快,适合部署到边缘设备。相比 CRNN+CTC,LPRNet 少了循环层,训练更稳,收敛更快,在 CCPD 上通常几十个 epoch 就能到不错的准确率。

4.2 字符字典与 CTC 标签对齐

字符字典要覆盖省份简称、字母、数字,外加 CTC 的 blank 符。我一般把 blank 放在索引 0,省份从 1 开始,字母和数字依次排。训练时标签是字符索引序列,CTC 会自动做对齐,不需要逐字符标注位置。这里有个容易翻车的点:字典顺序必须和生成标签时的映射一致,否则训练 loss 降不下去,识别出来全是乱码。建议把字典单独存成plate_dict.py,训练和推理都 import 同一份。

# plate_dict.py blank = '_' provinces = ["皖", "沪", "津", "渝", "冀", "晋", "蒙", "辽", "吉", "黑", "苏", "浙", "京", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "桂", "琼", "川", "贵", "云", "藏", "陕", "甘", "青", "宁", "新"] alphabets = ['A','B','C','D','E','F','G','H','J','K','L','M','N','P','Q','R','S','T','U','V','W','X','Y','Z'] ads = ['0','1','2','3','4','5','6','7','8','9'] chars = [blank] + provinces + alphabets + ads char_to_idx = {c: i for i, c in enumerate(chars)} idx_to_char = {i: c for c, i in char_to_idx.items()}

4.3 训练参数与常见收敛问题

LPRNet 训练用 CTC Loss,优化器选 Adam,学习率 1e-3,batch 32 左右。输入尺寸 94×24,灰度或 RGB 都行,CCPD 是彩色图,我一般转灰度减少计算量。训练时重点看 CTC loss 是否稳定下降,如果 loss 震荡大,把学习率降到 5e-4 或加梯度裁剪。另一个常见问题是过拟合,CCPD 里某些省份车牌特别多,模型会偏向这些省份,解决办法是在采样时做类别平衡,或者对稀有省份做数据增强。

import torch import torch.nn as nn ctc_loss = nn.CTCLoss(blank=0, reduction='mean') optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for imgs, labels, label_lens in dataloader: logits = model(imgs) # shape: [batch, T, num_classes] log_probs = logits.log_softmax(2).permute(1, 0, 2) # CTC 需要 [T, batch, classes] input_lens = torch.full((imgs.size(0),), log_probs.size(0), dtype=torch.long) loss = ctc_loss(log_probs, labels, input_lens, label_lens) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step()

这段里blank=0必须和字典里 blank 的索引一致,log_softmax(2)是在类别维度做,permute把时间维换到最前。clip_grad_norm_阈值 5.0 是防止梯度爆炸的常用值。如果 loss 降到 0.1 以下还在降,但验证集准确率不涨,就是过拟合了,早点停。

5. 避坑与排查:从数据到推理的五个血泪经验

5.1 现象:YOLO 训练 loss 正常但 mAP 极低

原因通常是标注格式不对。CCPD 转 YOLO 时,如果坐标没归一化,或者 x1>x2、y1>y2 没交换,YOLO 读进去框全是乱的。解决方法是转换后随机抽 20 张用labelImg打开看,确认框贴合车牌。另外检查ccpd.yaml里nc和names数量是否一致,不一致会直接报错或静默忽略类别。

5.2 现象:LPRNet 识别结果全是同一个字符

这是 CTC 训练里最经典的翻车。原因一般是字典映射错了,或者标签里混入了不在字典里的字符。排查时先把idx_to_char打印出来,再拿几条训练标签手动解码,看能不能还原成正确车牌。如果标签里有空格或换行没 strip 掉,也会导致映射失败。解决就是统一在生成标签时做strip()和字符校验。

5.3 现象:推理时检测框对但识别错位

车牌有倾斜时,YOLO 给的矩形框会包含较多背景,LPRNet 对字符位置敏感,容易把边缘噪声识别成字符。常见做法是先做透视变换把车牌摆正,再送 LPRNet。CCPD 字段 3 有四角点坐标,训练识别模型时可以用它做矫正,推理时如果没有角点,就用最小外接矩形加仿射变换近似。

5.4 现象:conda 环境里 YOLOv5 跑着跑着报 CUDA error

多半是 PyTorch 版本和显卡驱动不匹配,或者显存被其他进程占了。先nvidia-smi看显存占用,再确认torch.version.cuda和驱动支持的 CUDA 版本一致。如果 batch 设太大,也会在训练中途 OOM,把 batch 减半或开--amp混合精度能缓解。

5.5 现象:CCPD 某些子集图片读入为 None

CCPD 里有个别文件损坏或路径含特殊字符,cv2.imread返回 None。转换脚本里必须加if img is None: continue,否则后续img.shape直接崩。另外 Windows 下路径分隔符和 Linux 不同,用os.path.join拼路径,别手写斜杠。

6. 验证与进阶:用测试集量化评估,再谈端到端串联技巧

训练完不能只看 loss,得用留出的测试集算指标。检测这边算 mAP@0.5 和召回率,识别这边算整牌准确率和字符准确率。整牌准确率要求七位全对,字符准确率按位算,后者通常比前者高 5~10 个百分点。我一般写个评估脚本,把检测和识别串起来跑一遍测试集:

correct_plate = 0 total = 0 for img_path, gt_plate in test_list: img = Image.open(img_path) det = model(img) if len(det.xyxy[0]) == 0: continue x1, y1, x2, y2 = map(int, det.xyxy[0][0][:4].tolist()) crop = img.crop((x1, y1, x2, y2)).convert('L').resize((94, 24)) pred = lprnet_infer(crop) # 返回解码后的字符串 total += 1 if pred == gt_plate: correct_plate += 1 print(f'整牌准确率: {correct_plate / total:.4f}')

这段代码里lprnet_infer封装了预处理、前向、CTC 贪心解码。贪心解码实现简单,但遇到重复字符可能出错,比如皖A88888里连续 8 容易被 CTC 合并,这时候可以用 beam search 解码,或者在后处理里加规则校验。实际部署时,如果检测和识别分开跑,中间裁剪和 resize 的耗时可能比模型本身还大,我一般把预处理写成 batch 操作,一次处理多张裁图。

另一个进阶技巧是联合微调:先用 CCPD 分别训好检测和识别,再拿一小批真实场景数据,固定检测模型,只微调 LPRNet 的最后几层,让识别模型适应自己场景的字体和光照。这样比从头训省时间,效果也比直接用 CCPD 模型好。从那以后我每次上新场景,都强制先跑一遍测试集评估,再决定要不要微调,绝不凭感觉直接上线。希望帮到你。

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

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

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

立即咨询