YOLOv5+LPRNet车牌识别实战:从CCPD数据集训练到部署避坑

发布时间:2026/10/2 18:17:49
YOLOv5+LPRNet车牌识别实战:从CCPD数据集训练到部署避坑 简介这份资源面向计算机视觉入门与进阶开发者提供一套基于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. 从一张歪斜车牌说起YOLOv5LPRNet 到底解决什么问题停车场出口的相机拍下一张车牌角度偏了 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 python3.8 -y conda activate yolov5-lpr # 安装 PyTorch根据你的 CUDA 版本选这里以 CUDA 11.3 为例 pip install torch1.12.1cu113 torchvision0.13.1cu113 --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 倍以上。参数上python3.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-226469_448548-444552_231560_223470_436462-0_0_22_27_27_33_16-64-88.jpg各段含义如下字段位置含义示例值第1段面积比025第2段水平倾斜度、垂直倾斜度95_113第3段边界框左上右下坐标226469_448548第4段车牌四个角点坐标444552_231560_223470_436462第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_width720, img_height1160): 将 CCPD 文件名解析为 YOLO 格式标注 parts filename.split(-) # 第3段是边界框x1y1_x2y2 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 f0 {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_okTrue) 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:2CCPD 的ccpd_base有 20 万张取 5 万张训练、1 万张验证就够。注意图片和标签文件名必须一一对应放在images/train和labels/train下。常见错误是标签路径写成了绝对路径但 YOLOv5 会拼接path导致找不到文件。3. YOLOv5 车牌检测训练超参数怎么调、模型怎么选3.1 选 yolov5s 还是 yolov5m精度与速度的权衡车牌检测只有一个类别目标特征明显不需要太深的网络。我实测下来模型mAP0.5推理速度 (1080Ti)模型大小yolov5n0.9822.1ms3.9MByolov5s0.9913.5ms14.4MByolov5m0.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_h0.015、hsv_s0.7、hsv_v0.4比默认的 high 配置更稳。--cache把图片缓存到内存20 万张图建议别开内存会爆5 万张可以开。训练过程中重点看mAP0.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 透视变换把歪车牌摆正再送进 LPRNetYOLOv5 输出的是矩形框但 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 是字符类别数包含 blank18 是序列长度。然后接 CTC 损失做序列对齐。import torch import torch.nn as nn class LPRNet(nn.Module): def __init__(self, num_classes68): 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_classes68对应 CCPD 的字符集包括省份简称、字母、数字和 blank。CTC 损失要求输入序列长度大于等于标签长度18 对于 7 位车牌够用。参数上如果识别新能源 8 位车牌需要把序列长度加到 20 以上。训练时用nn.CTCLoss()注意blank索引要和num_classes-1对应。4.3 训练 LPRNet 与解码验证criterion nn.CTCLoss(blank67, zero_infinityTrue) optimizer torch.optim.Adam(model.parameters(), lr1e-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, dtypetorch.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_infinityTrue防止 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 转换时如果图片尺寸读错归一化坐标会大于 1YOLOv5 直接丢弃这些样本。解决用python -c import cv2; print(cv2.imread(xxx.jpg).shape)确认实际尺寸重新跑转换脚本。然后随机抽 10 个标签文件检查坐标是否都在 0 到 1 之间。5.2 现象LPRNet 识别结果全是重复字符或空原因CTC 解码时 blank 索引设错了。CCPD 字符集有 67 个字符加上 blank 是 68blank 索引应该是 67。如果设成 0解码会把所有字符都吞掉。解决确认nn.CTCLoss(blank67)和num_classes68一致。训练时打印一下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, 418)前 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 张图做测试统计端到端识别率检测框 IoU0.5 且字符完全正确。我一般会写个脚本自动跑输出混淆矩阵看哪些字符最容易错。常见的是0和O、1和I可以在后处理里加规则修正。最后说个血泪经验别在训练集上调参调到 99.9% 就以为成了实际场景的脏数据、逆光、遮挡才是真正的考验。我习惯留 10% 的 CCPD 数据不参与训练专门做最终验证这样心里有底。希望帮到你。本文还有配套的精品资源点击获取