ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

YOLOv5+LPRNet车牌检测识别实战:CCPD数据集训练与边缘部署

2026/9/28 14:53:49 拓冰建站 浏览量
YOLOv5+LPRNet车牌检测识别实战:CCPD数据集训练与边缘部署 简介这份资源面向计算机、电子信息、数学等专业的大学生及算法初学者提供一套基于YOLOv5s与LPRNet的轻量级中文车牌检测与识别完整方案可用于课程设计、期末大作业或毕业设计参考。项目以CCPD数据集为基础YOLOv5s负责车牌定位LPRNet完成字符识别目前支持蓝牌与绿牌后续可通过微调数据扩展更多车牌类型与场景。压缩包共61个文件约16.75MB包含22个Python脚本训练、测试、数据转换与推理、27张jpg与3张png示例图、3个yaml配置、1个pth与1个pt权重文件以及pptx、docx、md说明文档覆盖从数据准备到模型部署的完整流程。已有482人学习下载。读者可据此掌握检测与识别两阶段串联思路直接复用训练脚本、权重与配置快速复现车牌识别效果并在此基础上做二次开发。1. 车牌检测加识别这条链路为什么值得用 YOLOv5 和 LPRNet 拆开做如果你手头有一批卡口、停车场或者园区门禁抓拍的车牌图想做成一个能跑在边缘盒子上的检测加识别系统大概率绕不开两个名字YOLOv5 和 LPRNet。前者负责在整张图里把车牌框出来后者负责把框里的字符读出来。这套组合在 CCPD 数据集上被反复验证过属于工业界比较稳的落地路径。它适合谁适合已经会一点 Python、装过 PyTorch、想从零训一个车牌模型但不想被各种论文绕晕的工程师。整条链路的核心价值在于分工明确检测只管找位置识别只管读字符两边可以独立调参、独立换模型出了问题也容易定位是框歪了还是字读错了。CCPD 数据集提供了足够多的真实场景样本覆盖了白天、夜间、倾斜、模糊、雨雪等工况拿它当起点能省掉大量自己标数据的时间。2. 把 CCPD 数据集拆成 YOLOv5 能吃的检测格式2.1 CCPD 的目录结构和标注逻辑CCPD 原始数据不是标准的 YOLO 格式它的文件名里编码了车牌的位置、四个角点、车牌号码等信息。常见做法是先把文件名解析成结构化数据再转成 YOLO 需要的归一化中心点和宽高。CCPD 的文件名格式大致是「区域-倾斜角度-边界框-四个角点-车牌号码-亮度-模糊度.jpg」每一段用短横线分隔。解析的时候要特别注意角点顺序不同批次的数据可能顺序不一致直接按固定索引取会翻车。我一般会先写个小脚本统计一下文件名段数确认格式统一后再批量处理。2.2 从文件名到 YOLO 标签的转换脚本下面这段代码把 CCPD 文件名解析成 YOLO 格式的标签文件每个图对应一个 txt每行是「类别 x_center y_center width height」坐标都归一化到 0 到 1。import os import cv2 import numpy as np def parse_ccpd_filename(filename): 解析 CCPD 文件名返回边界框和车牌号 name os.path.splitext(filename)[0] parts name.split(-) # 边界框在第三段格式为 左上x左上y_右下x右下y bbox_str parts[2] left_top, right_bottom bbox_str.split(_) x1, y1 map(int, left_top.split()) x2, y2 map(int, right_bottom.split()) # 车牌号在第五段 plate parts[4] return (x1, y1, x2, y2), plate def convert_to_yolo(img_dir, label_dir): os.makedirs(label_dir, exist_okTrue) for fname in os.listdir(img_dir): if not fname.endswith(.jpg): continue img_path os.path.join(img_dir, fname) img cv2.imread(img_path) if img is None: continue h, w img.shape[:2] (x1, y1, x2, y2), plate parse_ccpd_filename(fname) # 归一化中心点和宽高 cx (x1 x2) / 2.0 / w cy (y1 y2) / 2.0 / h bw (x2 - x1) / w bh (y2 - y1) / h # 类别 0 表示车牌 line f0 {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n label_path os.path.join(label_dir, os.path.splitext(fname)[0] .txt) with open(label_path, w) as f: f.write(line) if __name__ __main__: convert_to_yolo(./ccpd_images, ./ccpd_labels)这段代码的关键点有三个一是parse_ccpd_filename里对文件名分段的理解如果数据来源不同段数可能变化建议先打印几条确认二是归一化时用原图宽高不要用 resize 后的尺寸三是类别统一设为 0因为检测阶段只关心「是不是车牌」这一个类。转换完成后建议随机抽几张图用可视化脚本画框检查确认没有整体偏移或镜像问题。2.3 划分训练集验证集和 data.yaml 配置CCPD 数据量大通常按 8:1:1 划分训练、验证、测试。划分时要注意同一辆车的不同帧尽量分到同一集合避免验证集里出现训练集见过的车牌导致指标虚高。常见做法是按文件名前缀分组后再随机划分。data.yaml 里写清楚 train、val、nc、names 四个字段nc 为 1names 为 [plate]。路径建议用绝对路径避免训练时找不到文件。train: /data/ccpd/train/images val: /data/ccpd/val/images nc: 1 names: [plate]配置完成后可以用 YOLOv5 自带的train.py先跑一个 epoch 看看数据加载是否正常如果报「No labels found」多半是路径或文件名对不上。3. 用 YOLOv5 训练车牌检测模型参数怎么设、什么时候停3.1 模型选型和输入尺寸的取舍YOLOv5 有 n、s、m、l、x 五个规格车牌检测属于单类小目标通常 n 或 s 就够了。如果部署在算力有限的设备上比如树莓派 5 或者低端边缘盒子直接选 yolov5n输入尺寸 640 或 416。输入尺寸越大小车牌召回越高但速度下降明显。我一般先在 640 下训一版看指标如果远距离车牌漏检多再考虑切到 1280 或者用切片推理。预训练权重建议加载 yolov5s.pt 或 yolov5n.pt从头训收敛慢且容易过拟合。3.2 关键超参数和训练命令下面这条命令是我在单卡 3090 上常用的配置batch 根据显存调整workers 根据 CPU 核数调整。python train.py \ --img 640 \ --batch 32 \ --epochs 100 \ --data data/ccpd.yaml \ --weights yolov5s.pt \ --cfg models/yolov5s.yaml \ --hyp data/hyps/hyp.scratch-low.yaml \ --name ccpd_yolov5s \ --cache参数说明--img 640是训练和验证的输入尺寸--batch 32在 24G 显存下比较稳显存小就降到 16 或 8--epochs 100对 CCPD 这种量级通常够用看 mAP 曲线如果 80 轮后还在涨可以加到 150--cache把图片缓存到内存加快读取但内存小于 32G 慎用。训练过程中重点看metrics/mAP_0.5和val/box_loss如果 box_loss 震荡不降多半是学习率偏大或者标注框有异常。3.3 训练过程中的监控和早停策略YOLOv5 默认每轮跑完验证结果存在runs/train/exp下。我一般会盯三个信号mAP_0.5 是否稳定上升、precision 和 recall 是否平衡、验证集 loss 是否在训练 loss 还在降的时候先升。如果出现验证 loss 连续 10 轮上升就可以考虑早停。YOLOv5 自带--patience参数设成 15 到 20 比较合理。另外CCPD 里夜间和模糊样本占比不低如果发现模型在暗光下漏检严重不要急着加数据先检查标注框是否在暗光图里也准确有时候是标注质量问题而不是模型问题。4. 用 LPRNet 做车牌字符识别从裁剪到序列解码4.1 LPRNet 为什么适合车牌识别LPRNet 是一个轻量级的端到端车牌识别网络它不需要先做字符分割直接输入车牌图输出字符序列。相比 CRNN 加 CTC 的方案LPRNet 更小更快在 CCPD 上精度也够用。它的核心结构是 backbone 加全局上下文模块再加 CTC 解码。输入尺寸通常是 94x24 或者 120x32保持宽高比接近真实车牌。训练时把检测框裁出来的车牌图缩放到固定尺寸送进网络输出每个位置的字符概率再用 CTC 损失对齐。4.2 车牌字符集定义和 CTC 解码逻辑CCPD 里的车牌主要是蓝牌和绿牌字符包括省份汉字、字母、数字。常见做法是把所有出现过的字符收集起来加一个 CTC blank 标签构成字符集。下面是一个字符集和编码解码的示例。# 字符集索引 0 留给 CTC blank CHARS [blank, 京, 沪, 粤, 苏, 浙, 鲁, 川, 渝, 冀, 豫, 云, 辽, 黑, 湘, 皖, 鲁, 新, 甘, 桂, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, G, H, J, K, L, M, N, P, Q, R, S, T, U, V, W, X, Y, Z] char_to_idx {c: i for i, c in enumerate(CHARS)} idx_to_char {i: c for i, c in enumerate(CHARS)} def decode_ctc(pred_indices): CTC 贪心解码去重去 blank result [] prev -1 for idx in pred_indices: if idx ! 0 and idx ! prev: result.append(idx_to_char[idx]) prev idx return .join(result)这里要注意字符集顺序一旦确定就不要改训练和推理必须用同一份。CTC 解码时先去掉连续重复再去掉 blank顺序不能反。如果发现识别结果里出现大量重复字符多半是 blank 标签没设对或者学习率太大导致输出不稳定。4.3 训练 LPRNet 的数据管道和损失函数训练 LPRNet 需要把检测框裁出来的车牌图统一 resize 到 94x24标签转成索引序列。损失用 CTC Loss优化器用 Adam初始学习率 1e-3每 20 轮降一半。下面是一个简化的训练循环片段。import torch import torch.nn as nn ctc_loss nn.CTCLoss(blank0, reductionmean) optimizer torch.optim.Adam(model.parameters(), lr1e-3) for epoch in range(100): model.train() for imgs, labels, label_lens in train_loader: imgs imgs.cuda() preds model(imgs) # 形状 [T, N, C] preds preds.log_softmax(2) input_lens torch.full((imgs.size(0),), preds.size(0), dtypetorch.long) loss ctc_loss(preds, labels, input_lens, label_lens) optimizer.zero_grad() loss.backward() optimizer.step()参数说明blank0对应字符集里的 blankpreds的形状是时间步在前、batch 在后、类别在最后如果模型输出形状不同要转置input_lens是每个样本的时间步长度LPRNet 通常固定。训练时如果 loss 降到 0.1 以下但验证准确率不涨多半是过拟合可以加 dropout 或者数据增强。5. 检测和识别串起来后处理、对齐和常见翻车点5.1 检测框裁剪和识别输入的预处理YOLOv5 输出的是归一化坐标要还原到原图尺寸再裁剪。裁剪时建议往外扩 5% 到 10%避免车牌边缘字符被切掉。裁剪后做灰度化、直方图均衡或者自适应二值化能提升暗光下的识别率。但要注意LPRNet 训练时用的预处理必须和推理时一致训练用彩色推理用灰度就会掉点。我一般会在训练 LPRNet 时也加入随机灰度化增强让模型对颜色不敏感。5.2 检测置信度和识别置信度的联合过滤实际部署时检测框的置信度和识别的平均字符概率要一起看。检测置信度低于 0.4 的框直接丢识别平均概率低于 0.6 的结果标记为可疑。如果同一张图里出现多个框按面积和位置做非极大值抑制保留最像车牌的那个。常见翻车场景是广告牌上的车牌图案被误检这时候可以加一个宽高比过滤车牌宽高比一般在 2.5 到 4.5 之间超出范围的框直接排除。5.3 避坑与排查五个血泪教训现象一训练 loss 正常但验证 mAP 一直是 0。原因data.yaml 里的路径写错或者标签文件名和图片名没对上。 解决用--verbose跑一轮看日志里加载了多少张图、多少标签数量对不上就逐级检查路径。现象二检测框整体偏移框到车牌旁边。原因CCPD 文件名解析时角点顺序搞反或者归一化用了 resize 后的尺寸。 解决抽 10 张图可视化对比原图标注和转换后的框确认坐标变换一致。现象三LPRNet 识别结果全是同一个字符。原因CTC blank 标签索引不是 0或者字符集里 blank 不在第一位。 解决检查CTCLoss(blank0)和字符集定义确保 blank 索引一致。现象四夜间车牌识别率骤降。原因训练集里夜间样本少或者预处理没有做暗光增强。 解决对夜间样本过采样训练时加随机亮度对比度扰动推理时加 CLAHE。现象五树莓派 5 上推理速度只有 2 FPS。原因模型没量化输入尺寸太大或者用了 CPU 推理。 解决导出 ONNX 后用 ONNX Runtime 或 OpenVINO输入降到 416开启 INT8 量化。6. 把模型塞进边缘设备导出、量化和一个提速技巧训练完的模型要落地第一步是导出。YOLOv5 用export.py导出 ONNXLPRNet 手动写导出脚本。导出时注意 opset 版本YOLOv5 建议 12LPRNet 建议 11。导出后用 onnxruntime 跑一遍对比 PyTorch 和 ONNX 的输出差异如果 mAP 掉超过 1 个点检查是否有不支持的自定义算子。python export.py --weights runs/train/ccpd_yolov5s/weights/best.pt \ --include onnx --opset 12 --img 640量化方面边缘设备优先考虑 INT8。ONNX Runtime 提供动态量化和静态量化静态量化需要校准集从验证集里抽 200 张图就够。量化后精度一般掉 0.5 到 1.5 个点如果掉太多检查校准集是否覆盖了夜间和模糊场景。树莓派 5 上还可以用 NCNN 或者 OpenVINO实测 NCNN 在 ARM 上比 ONNX Runtime 快 20% 左右。一个提速技巧是检测和识别异步流水线检测线程负责出框识别线程从队列里取框裁剪识别两个线程用队列解耦。这样即使识别稍慢检测也不会被阻塞整体吞吐能提升 30% 以上。代码上用一个queue.Queue(maxsize10)做缓冲检测线程 put识别线程 get注意加锁保护共享的模型实例。我自己踩过最深的坑是导出 ONNX 后忘了把 LPRNet 的输入尺寸写死导致动态轴推理时 CTC 解码长度对不上识别结果错位。后来养成习惯导出后一定用固定尺寸跑一遍端到端测试确认检测框和识别结果能对上。希望帮到你。本文还有配套的精品资源点击获取