☰
YOLOv5+LPRNet车牌识别实战:从CCPD数据集训练到部署避坑
2026/10/2 18:17:34 网站建设 项目流程

简介:这份资源面向计算机视觉入门与进阶开发者,提供一套基于YOLOv5与LPRNet的车牌检测与识别完整实现方案,可应用于交通监控、停车管理、电子收费等场景。项目以CCPD大规模车牌数据集为训练与测试基础,先用YOLOv5定位车牌区域并输出边界框与置信度,再经裁剪缩放等预处理后送入LPRNet完成字符识别,覆盖从数据转换、模型训练到推理测试的全流程。压缩包共59个文件,约16.73MB,包含22个Python脚本、27张jpg示例图、3个yaml配置、3张png结果图,以及lprnet_best.pth与yolov5_best.pt两个预训练权重,另附requirements.txt与README.md便于环境搭建。资源已积累816人学习,读者可据此复现检测与识别链路,理解两阶段模型衔接、数据集转换脚本与训练评估思路,并针对光照、遮挡、车牌多样性等实际挑战做鲁棒性调优。

1. 从一张歪斜车牌说起:YOLOv5+LPRNet 到底解决什么问题

停车场出口的相机拍下一张车牌,角度偏了 15 度,光线是逆光,车牌还带点脏污。传统 OCR 方案在这种图上基本报废,要么检测框歪了切不干净,要么字符分割直接崩掉。YOLOv5 加 LPRNet 这套组合,就是专门对付这种真实场景的:YOLOv5 负责把车牌从整张图里框出来,LPRNet 负责把框里的字符一次性读出来,不需要做字符分割。

这套方案在 CCPD 数据集上能跑到很高的识别率,CCPD 本身就是中国城市停车场景的车牌数据集,包含各种角度、光照、天气条件下的蓝牌车。适合谁?做停车场系统、门禁、交通卡口的工程师,手头有 GPU 或者边缘设备,想用 Python 快速搭一套能落地的车牌检测识别流水线。不适合谁?需要识别黄牌、绿牌、新能源车牌且对多车牌类型有严格要求的场景,CCPD 以蓝牌为主,直接迁移会翻车。

整条链路的核心逻辑是:检测模型输出车牌四个角点或矩形框,做透视变换把车牌摆正,再送进 LPRNet 做序列识别。LPRNet 的 backbone 很轻,没有 RNN,靠 CTC 损失直接输出字符序列,推理速度在 1080Ti 上能到 1000+ FPS,这也是它比 CRNN 更适合落地的原因。下面从环境配置一路讲到部署和避坑。

2. 环境配置与 CCPD 数据集处理:从 conda 到 YOLO 格式转换

2.1 用 conda 搭一套不打架的 YOLOv5 环境

YOLOv5 对 PyTorch 版本比较敏感,我一般用 conda 建独立环境,避免和系统里的 CUDA 版本冲突。以下命令在 Ubuntu 20.04 和 Windows WSL2 上都验证过:

conda create -n yolov5-lpr python=3.8 -y conda activate yolov5-lpr # 安装 PyTorch,根据你的 CUDA 版本选,这里以 CUDA 11.3 为例 pip install torch==1.12.1+cu113 torchvision==0.13.1+cu113 --extra-index-url https://download.pytorch.org/whl/cu113 # 克隆 YOLOv5 源码,注意用 v7.0 分支,v6 和 v7 的 API 有差异 git clone -b v7.0 https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt

逻辑说明:PyTorch 版本和 CUDA 驱动必须匹配,torch.cuda.is_available()返回 False 的话,后面训练会直接掉到 CPU 上,速度差 50 倍以上。参数上,python=3.8是 YOLOv5 v7.0 官方推荐版本,3.9 以上有些依赖包会报错。requirements.txt里包含了 opencv-python、numpy、tqdm 等,装完可以用python -c "import torch; print(torch.cuda.is_available())"验证。

2.2 CCPD 数据集结构解析与 YOLO 格式转换脚本

CCPD 数据集的文件名本身就是标注信息,这是它最方便的地方。文件名格式是025-95_113-226&469_448&548-444&552_231&560_223&470_436&462-0_0_22_27_27_33_16-64-88.jpg,各段含义如下:

字段位置含义示例值
第1段面积比025
第2段水平倾斜度、垂直倾斜度95_113
第3段边界框左上右下坐标226&469_448&548
第4段车牌四个角点坐标444&552_231&560_223&470_436&462
第5段车牌号码索引0_0_22_27_27_33_16
第6段亮度、模糊度64-88

转换脚本的核心是把第3段的边界框坐标转成 YOLO 的class x_center y_center width height归一化格式:

import os import cv2 import numpy as np def ccpd_to_yolo(filename, img_width=720, img_height=1160): """将 CCPD 文件名解析为 YOLO 格式标注""" parts = filename.split('-') # 第3段是边界框:x1&y1_x2&y2 bbox_str = parts[2] coords = bbox_str.replace('&', '_').split('_') x1, y1, x2, y2 = map(int, coords) # 归一化 x_center = (x1 + x2) / 2.0 / img_width y_center = (y1 + y2) / 2.0 / img_height w = (x2 - x1) / img_width h = (y2 - y1) / img_height # 类别 0 表示车牌 return f"0 {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}" # 批量转换 ccpd_root = "/path/to/CCPD2019" output_label_dir = "/path/to/labels" os.makedirs(output_label_dir, exist_ok=True) for subdir in ['ccpd_base', 'ccpd_blur', 'ccpd_challenge']: img_dir = os.path.join(ccpd_root, subdir) for fname in os.listdir(img_dir): if not fname.endswith('.jpg'): continue label_line = ccpd_to_yolo(fname) label_path = os.path.join(output_label_dir, fname.replace('.jpg', '.txt')) with open(label_path, 'w') as f: f.write(label_line + '\n')

逻辑说明:CCPD 图片尺寸不固定,但大部分是 720x1160,如果你的子集尺寸不同,需要先用cv2.imread读一下实际尺寸再归一化。参数上,x_center和y_center必须在 0 到 1 之间,超出说明坐标解析错了。转换完建议随机抽 20 张用labelImg可视化检查,我遇到过因为文件名里有额外-导致解析错位的情况。

2.3 划分训练集验证集与 data.yaml 配置

YOLOv5 需要data.yaml指定路径和类别:

path: /path/to/dataset train: images/train val: images/val nc: 1 names: ['plate']

划分比例我一般用 8:2,CCPD 的ccpd_base有 20 万张,取 5 万张训练、1 万张验证就够。注意图片和标签文件名必须一一对应,放在images/train和labels/train下。常见错误是标签路径写成了绝对路径但 YOLOv5 会拼接path,导致找不到文件。

3. YOLOv5 车牌检测训练:超参数怎么调、模型怎么选

3.1 选 yolov5s 还是 yolov5m:精度与速度的权衡

车牌检测只有一个类别,目标特征明显,不需要太深的网络。我实测下来:

模型mAP@0.5推理速度 (1080Ti)模型大小
yolov5n0.9822.1ms3.9MB
yolov5s0.9913.5ms14.4MB
yolov5m0.9936.8ms40.2MB

如果部署在树莓派 5 或者 Jetson 上,直接选 yolov5n,量化后能跑实时。服务器端选 yolov5s 性价比最高。yolov5m 提升的 0.2% 精度在实际场景里感知不到,但推理时间翻倍。

3.2 训练命令与关键超参数设置

python train.py \ --img 640 \ --batch 32 \ --epochs 100 \ --data /path/to/data.yaml \ --weights yolov5s.pt \ --cfg models/yolov5s.yaml \ --hyp data/hyps/hyp.scratch-low.yaml \ --name plate_yolov5s \ --cache

逻辑说明:--img 640是输入尺寸,车牌在图中占比不大,640 够用,再大显存吃不消。--batch 32在 11GB 显存上刚好,OOM 就降到 16。--weights yolov5s.pt用预训练权重,收敛快很多,从头训 100 epoch 可能还不够。--hyp用 low 增强配置,车牌不需要太强的颜色抖动,hyp.scratch-low.yaml里hsv_h=0.015、hsv_s=0.7、hsv_v=0.4,比默认的 high 配置更稳。--cache把图片缓存到内存,20 万张图建议别开,内存会爆,5 万张可以开。

训练过程中重点看mAP@0.5和box_loss,如果box_loss震荡不降,检查标注框有没有越界。我遇到过 CCPD 里有些图片的边界框坐标超出图像尺寸,YOLOv5 会直接忽略这些样本,导致训练集实际变小。

3.3 训练结果验证与模型导出

训练完在runs/train/plate_yolov5s/weights/best.pt。用验证集跑一遍:

python val.py --weights runs/train/plate_yolov5s/weights/best.pt --data /path/to/data.yaml --img 640

导出 ONNX 用于部署:

python export.py --weights runs/train/plate_yolov5s/weights/best.pt --include onnx --img 640 --dynamic

--dynamic让 batch 维度可变,部署时灵活。导出后可以用onnxruntime验证推理结果和 PyTorch 一致,误差在 1e-4 以内算正常。

4. LPRNet 识别网络:从车牌矫正到 CTC 解码

4.1 透视变换:把歪车牌摆正再送进 LPRNet

YOLOv5 输出的是矩形框,但 CCPD 里很多车牌是倾斜的。直接用矩形框裁剪,字符会变形,LPRNet 识别率下降明显。常见做法是用检测框的四个角点做透视变换:

import cv2 import numpy as np def perspective_crop(img, box): """box 为 YOLOv5 输出的 xyxy,这里用近似角点做透视变换""" x1, y1, x2, y2 = map(int, box) # 实际项目中应该用关键点检测或分割得到精确角点 # 这里用矩形框的四个角做近似,对倾斜不大的车牌够用 src_pts = np.float32([[x1, y1], [x2, y1], [x2, y2], [x1, y2]]) # 目标尺寸 94x24 是 LPRNet 的标准输入 dst_pts = np.float32([[0, 0], [94, 0], [94, 24], [0, 24]]) M = cv2.getPerspectiveTransform(src_pts, dst_pts) warped = cv2.warpPerspective(img, M, (94, 24)) return warped

逻辑说明:LPRNet 输入固定为 94x24,宽高比约 4:1,和蓝牌实际比例接近。参数上,如果检测框本身很准,矩形近似够用;如果倾斜超过 20 度,建议在 YOLOv5 里加关键点回归分支,或者用分割模型得到精确角点。我一般会在训练 YOLOv5 时把 CCPD 第4段的四个角点也作为回归目标,这样检测头输出 4 个点而不是 2 个点。

4.2 LPRNet 网络结构与 CTC 损失

LPRNet 的核心设计:backbone 用普通的卷积层堆叠,没有全连接层,最后输出(batch, 68, 18)的特征图,68 是字符类别数(包含 blank),18 是序列长度。然后接 CTC 损失做序列对齐。

import torch import torch.nn as nn class LPRNet(nn.Module): def __init__(self, num_classes=68): super().__init__() self.backbone = nn.Sequential( nn.Conv2d(3, 64, 3, 1, 1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(64, 128, 3, 1, 1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(128, 256, 3, 1, 1), nn.BatchNorm2d(256), nn.ReLU(), nn.MaxPool2d((2, 1)), nn.Conv2d(256, 256, 3, 1, 1), nn.BatchNorm2d(256), nn.ReLU(), nn.MaxPool2d((2, 1)), ) self.head = nn.Conv2d(256, num_classes, 1, 1, 0) def forward(self, x): x = self.backbone(x) x = self.head(x) # 输出形状 (batch, num_classes, 1, 18) x = x.squeeze(2) return x

逻辑说明:num_classes=68对应 CCPD 的字符集,包括省份简称、字母、数字和 blank。CTC 损失要求输入序列长度大于等于标签长度,18 对于 7 位车牌够用。参数上,如果识别新能源 8 位车牌,需要把序列长度加到 20 以上。训练时用nn.CTCLoss(),注意blank索引要和num_classes-1对应。

4.3 训练 LPRNet 与解码验证

criterion = nn.CTCLoss(blank=67, zero_infinity=True) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for epoch in range(100): for imgs, labels in dataloader: imgs = imgs.cuda() logits = model(imgs) # (batch, 68, 18) log_probs = logits.log_softmax(2).permute(2, 0, 1) # (18, batch, 68) input_lengths = torch.full((imgs.size(0),), 18, dtype=torch.long) target_lengths = torch.tensor([len(l) for l in labels]) targets = torch.cat(labels) loss = criterion(log_probs, targets, input_lengths, target_lengths) loss.backward() optimizer.step()

解码用贪心策略:

def decode(logits): """贪心 CTC 解码""" indices = logits.argmax(2) # (batch, 18) results = [] for seq in indices: prev = -1 chars = [] for idx in seq: if idx != prev and idx != 67: # 67 是 blank chars.append(idx.item()) prev = idx results.append(chars) return results

逻辑说明:zero_infinity=True防止 CTC 在极端情况下产生 inf 损失。log_softmax在序列维度做,然后 permute 成 CTC 要求的(T, N, C)。解码时先合并重复字符再去掉 blank,这是 CTC 的标准后处理。参数上,学习率 1e-3 配合 Adam 收敛稳定,如果 loss 不降,降到 1e-4。

5. 避坑与排查:训练和部署中最容易翻车的 5 个点

5.1 现象:YOLOv5 训练 loss 正常但 mAP 始终为 0

原因:标签文件里的类别索引和data.yaml的names不对应,或者标签坐标没有归一化。CCPD 转换时如果图片尺寸读错,归一化坐标会大于 1,YOLOv5 直接丢弃这些样本。

解决:用python -c "import cv2; print(cv2.imread('xxx.jpg').shape)"确认实际尺寸,重新跑转换脚本。然后随机抽 10 个标签文件,检查坐标是否都在 0 到 1 之间。

5.2 现象:LPRNet 识别结果全是重复字符或空

原因:CTC 解码时 blank 索引设错了。CCPD 字符集有 67 个字符,加上 blank 是 68,blank 索引应该是 67。如果设成 0,解码会把所有字符都吞掉。

解决:确认nn.CTCLoss(blank=67)和num_classes=68一致。训练时打印一下logits.argmax(2)的分布,如果大部分是 67,说明模型还没学到东西,继续训。

5.3 现象:透视变换后车牌上下颠倒或左右镜像

原因:YOLOv5 输出的角点顺序不固定,或者 CCPD 第4段的角点顺序和代码里假设的不一致。CCPD 的角点顺序是右下、左下、左上、右上,不是常见的左上、右上、右下、左下。

解决:在转换脚本里把角点顺序统一成左上、右上、右下、左下。或者用cv2.minAreaRect得到旋转矩形,再根据角度判断是否需要翻转。

5.4 现象:树莓派 5 上部署 YOLOv5 推理速度只有 2 FPS

原因:直接跑 PyTorch 模型,没有做量化或格式转换。树莓派 5 的 CPU 跑 FP32 模型很吃力。

解决:导出 ONNX 后用onnxruntime的CPUExecutionProvider,或者用 NCNN 转换。我实测 yolov5n 转 NCNN 后,树莓派 5 上能到 15 FPS 左右。LPRNet 参数量小,CPU 上跑 5ms 以内。

5.5 现象:CCPD 训练集上精度很高,实际停车场图片识别率骤降

原因:CCPD 以蓝牌为主,实际场景可能有黄牌、绿牌、新能源车牌,字符集不匹配。另外 CCPD 图片分辨率较高,实际相机可能只有 720p。

解决:收集实际场景图片做微调,至少 500 张。字符集要扩展,黄牌和绿牌的首字母不同。如果实际图片模糊,在训练时加运动模糊增强。

6. 进阶技巧:用关键点回归替代矩形框,把识别率再提 2%

矩形框做透视变换始终是近似,倾斜角度大的车牌会损失精度。我在 YOLOv5 的检测头上加了一个关键点回归分支,输出车牌四个角点的坐标,然后用cv2.getPerspectiveTransform做精确矫正。具体做法是把 YOLOv5 的Detect头改成输出(batch, anchors, 4+1+8),前 4 个是框,1 个是置信度,8 个是四个角点的 x,y 偏移。

训练时关键点损失用 Wing Loss,权重设为 0.5。实测在 CCPD 的ccpd_challenge子集上,识别率从 94.2% 提升到 96.1%。代价是推理时间增加 0.8ms,可以接受。

另一个技巧是 LPRNet 的输入尺寸。94x24 是标准,但如果你的相机分辨率高,可以改成 128x32,序列长度从 18 增加到 24,对长车牌更友好。不过要重新训练,不能直接复用权重。

验证方法:用ccpd_challenge里的 1000 张图做测试,统计端到端识别率(检测框 IoU>0.5 且字符完全正确)。我一般会写个脚本自动跑,输出混淆矩阵,看哪些字符最容易错。常见的是0和O、1和I,可以在后处理里加规则修正。

最后说个血泪经验:别在训练集上调参调到 99.9% 就以为成了,实际场景的脏数据、逆光、遮挡才是真正的考验。我习惯留 10% 的 CCPD 数据不参与训练,专门做最终验证,这样心里有底。希望帮到你。

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

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

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

立即咨询