首页 / 资讯中心 / 文章详情

YOLOv5+LPRNet:基于CCPD的车牌识别两段式实战方案

YOLOv5+LPRNet:基于CCPD的车牌识别两段式实战方案 ★ FEATURED ARTICLE
简介项目压缩包是一套基于YOLOv5s与LPRNet的中国车牌检测与识别完整工程面向计算机、电子信息、数学等专业学生可作为课程设计、期末大作业或毕业设计的参考资料也适合作为计算机视觉与OCR入门实战案例。检测端由YOLOv5s完成车牌定位识别端由LPRNet完成字符序列识别当前支持蓝牌及新能源绿牌后续可添加数据微调以支持更多场景和类型。资源共61个文件包含22个Python脚本涵盖YOLOv5与LPRNet的训练、测试、CCPD数据集转换及数据集划分等附带27张样例图片、预训练权重pt与pth、yaml配置说明以及docx、pptx、md格式的项目文档压缩包整体约16.75MB。该工程已有483人学习使用。借助预训练权重可直接跑通检测与识别流程也可依据自身数据对模型微调整体目录按功能划分为weights、models、utils、demo等模块结构清晰适合实际动手复现或二次扩展。1. 车牌识别项目为什么非要“检测 识别”两段式CCPD 与 YOLOv5、LPRNet 的配合逻辑提起车牌识别很多新手第一反应是“直接拿一张图丢给模型让它输出车牌号”但真实路侧抓拍、停车场出入口、高速卡口的图像里车牌往往只占画面的一小块周围有车灯眩光、保险杠纹理、复杂背景干扰。如果让一个端到端模型同时完成“找车牌”和“读字符”它对训练数据的构图要求极高换一个摄像头角度或者夜间补光不均匀召回率立刻崩。更稳的从业方案是把任务拆成两段先用 YOLOv5 做目标检测把车牌从整图中抠出来再用 LPRNet 对抠出来的车牌小图做字符序列识别。CCPD中国城市停车场数据集恰好同时覆盖检测和识别两个环节的标注需求是这类项目最常见的起步数据集之一。这套组合的优势在于两个模型可以独立调优、独立换数据检测模型做得差时不用重训识别模型反过来也一样。本文按这个思路讲清楚从数据集准备、模型训练到部署的完整落地路径以及我实际跑通时踩过的几个关键坑。2. CCPD 数据集到底怎么用检测标注与识别标注的取舍2.1 CCPD 的文件命名规则与标签隐藏位置CCPD 数据集的图片文件名并不是随机字符串而是一串以连字符分隔的字段例如“025-95_113-154383_386473-386473_177454_154383_363402-0_0_22_27_27_33_16-37-15.jpg”。拆开看第一段是车牌类型和区域代号第二段是亮度与对比度特征第三段是车牌的四个顶点坐标格式为 x1y1_x2y2_x3y3_x4y4注意不是常见的左上右下两点框第四段是车牌字符标签按顺序排列每个数字对应一个字符类别最后是车牌宽度和高度。这种命名方式意味着检测标注不需要额外的 XML 或 TXT 文件直接从文件名里解析坐标就能生成 YOLO 格式的标签。但要注意CCPD 的坐标是“四边形顶点”而 YOLOv5 默认使用“轴对齐矩形框”两者不能直接互换必须做外接矩形转换。2.2 从 CCPD 文件名生成 YOLOv5 标签的 Python 脚本我一般先把 CCPD 图片按一定比例划分训练集和验证集然后写一个解析脚本把每个文件名里的四边形坐标转成 YOLOv5 需要的归一化中心点格式。下面这个脚本可以直接跑前提是图片文件命名保持 CCPD 原始结构。import os import glob import random # 假设图片放在 ./ccpd_imgs 目录下 img_dir ./ccpd_imgs out_dir ./yolo_labels os.makedirs(out_dir, exist_okTrue) # CCPD 文件名示例: 025-95_113-154383_386473-...-37-15.jpg # 第三段是四个顶点坐标第四段是字符标签需要按需求取舍 def parse_ccpd_filename(filename): base os.path.basename(filename).replace(.jpg, ) parts base.split(-) # parts[2] 是四边形顶点形如 154383_386473_...四个点用下划线分隔 quad parts[2].split(_) xs, ys [], [] for point in quad: x_str, y_str point.split() xs.append(int(x_str)); ys.append(int(y_str)) xmin, xmax min(xs), max(xs) ymin, ymax min(ys), max(ys) # parts[3] 是字符标签用于识别训练后续单独处理 return (xmin, ymin, xmax, ymax), parts[3] # 转换为 YOLOv5 标签类别0 归一化中心点 宽高 def to_yolo_line(img_w, img_h, box): xmin, ymin, xmax, ymax box cx (xmin xmax) / 2 / img_w cy (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h return f0 {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n # 获取所有图片路径并划分这里简单按9:1划分 img_paths glob.glob(os.path.join(img_dir, *.jpg)) random.shuffle(img_paths) split_idx int(len(img_paths) * 0.9) train_paths img_paths[:split_idx] val_paths img_paths[split_idx:] # 每张图读取尺寸并写标签文件 for split_name, paths in [(train, train_paths), (val, val_paths)]: split_out os.path.join(out_dir, split_name) os.makedirs(split_out, exist_okTrue) for img_path in paths: # 这里用 PIL 读尺寸避免加载像素数据 from PIL import Image with Image.open(img_path) as im: img_w, img_h im.size box, _ parse_ccpd_filename(img_path) lbl_path os.path.join(split_out, os.path.basename(img_path).replace(.jpg, .txt)) with open(lbl_path, w) as f: f.write(to_yolo_line(img_w, img_h, box))这段脚本里的参数其实只有两个地方需要按实际改动一是图片目录路径二是划分比例。YOLOv5 训练时它会自己去图片目录对应的同级 labels 目录找同名 txt 文件所以脚本的输出目录最好放在images的兄弟目录下例如图片在datasets/ccpd/images/train标签就写在datasets/ccpd/labels/train。2.3 识别数据如何从 CCPD 中提取LPRNet 的输入是车牌小图输出是字符序列所以需要从原图把车牌区域裁出来。这里有个容易踩坑的点CCPD 文件名里的四边形坐标是原始图像坐标系下的如果先做了检测裁剪再用原坐标去裁识别训练图就会错位。正确做法是直接根据 CCPD 的标注坐标从原图裁切并把裁出来的车牌图统一缩放到 LPRNet 的输入尺寸常见做法是 94×24宽度 94高度 24。裁切时建议把四边形外接矩形往外扩几个像素避免车牌边缘字符被切断。扩展比例我一般取 0.1 到 0.15太小边缘字符容易缺太大背景噪声变多。裁完之后每个字符类别需要映射成一个数字 IDCCPD 的字符标签字段覆盖了省份简称缩写、字母和数字需要建立一张从字符到索引的映射表。映射表千万别手工敲直接从训练集里统计出现的字符集合自动生成否则漏掉某个汉字会让训练时索引越界。3. 用 YOLOv5 训练车牌检测器网络结构选择与超参数调整3.1 YOLOv5s 还是 YOLOv5m先看部署目标再定网络结构图YOLOv5 有 s、m、l、x 四个主要版本区别在网络的深度和宽度倍数。车牌检测目标小、类别单一不需要很大的模型容量。如果后续要部署到树莓派或 RK3568 这类边缘设备优先选 YOLOv5s如果是在办公电脑或服务器上跑YOLOv5m 会带来更稳的召回率。我在项目里通常先用 YOLOv5s 跑通基线确认检测精度不够再加 m。车牌这个场景下s 和 m 的 mAP 差距通常不到 2 个百分点但推理速度差距很大所以不要盲目追求大模型。网络结构图里真正需要关注的是检测头输出层的 strideYOLOv5 有三个检测层分别对应 8、16、32 倍下采样车牌这种中等尺寸目标主要靠中间那个 stride 16 的检测层如果发现小角度车牌漏检可以额外关注 stride 8 的特征层是否需要加强。3.2 YOLOv5 训练自己的数据集流程与关键参数训练前要准备好数据集目录结构并写一个数据集 yaml 文件。YOLOv5 的 data yaml 里只需要三行核心内容训练图片路径、验证图片路径、类别数量与名称。注意 CCPD 只有一类所以 nc 填 1names 写成[plate]。yaml 文件不要放在项目根目录之外否则相对路径解析会出错。然后运行训练命令python train.py \ --img 640 \ --batch 16 \ --epochs 100 \ --data CCPD.yaml \ --weights yolov5s.pt \ --cache \ --device 0参数说明--img 640是输入尺寸CCPD 原图分辨率较高直接缩放到 640 会丢失小目标细节但 YOLOv5 的 mosaic 增强会拼贴多张图所以 640 足够--batch 16取决于显存8GB 显存跑 s 模型用 16 没问题超过就降到 8--cache把图片提前缓存进显存能明显加速训练但需要额外显存显存紧张时不要开--weights yolov5s.pt是 COCO 预训练权重用它做迁移学习收敛速度比从零训练快得多。如果显存只有 4GB建议把 batch 设为 8img 降到 512并关闭 cache否则会直接 OOM。3.3 yolov5 超参数调优三个必须动的参数YOLOv5 的hyp.scratch.yaml里有一堆超参数但针对车牌检测我建议只动三个一是hsv_h、hsv_s、hsv_v这组数据增强参数二是fliplr车牌字符不能左右翻转必须设为 0否则模型会把“京A”学成“A京”的镜像三是mosaic默认是 1.0建议保持但训练最后 10 个 epoch 把 mosaic 关掉YOLOv5 的--multi-scale也建议关掉。具体调整方式是在训练命令中直接覆盖超参数文件里的值比如python train.py \ --data CCPD.yaml \ --weights yolov5s.pt \ --hyp hyp.scratch.yaml \ --epochs 100 \ --batch 16 \ --img 640 \ --device 0如果你用的是官方代码可以直接编辑data/hyps/hyp.scratch.yaml。将fliplr: 0.0改成 0hsv_h: 0.015保持或略微降低因为车牌颜色本身就是特征过度改变色相会让模型依赖颜色学习而不是学习字符边缘纹理不过稍微增强一点亮度对比度能提高夜间泛化能力。这些超参数的本质是控制训练时的“数据变形程度”调太狠模型学到的是畸变后的车牌调太弱又容易过拟合原始 CCPD需要根据验证集 mAP 反复试。3.4 检测效果验证哪些指标值得看训练结束后不要只看 mAP先跑一次验证集的混淆矩阵。车牌检测的误检通常来源于车灯、车身贴纸、保险杠镀铬条这些目标在 CCPD 里也有出现但占比不多。如果验证集上的 precision 很高但 recall 低说明模型漏掉了很多小角度或遮挡车牌此时优先检查conf_thres是否设得太高YOLOv5 验证默认置信度阈值是 0.25对于车牌这种“宁缺毋滥”的场景可以降到 0.1 观察。如果 mAP 不低但实际图像里车牌框抖动明显则需要回到数据集检查标注框是否贴合车牌边缘——CCPD 的四边形外接矩形有时候会把螺丝孔也框进去最好统一外扩 5 像素让识别模型拿到更完整的车牌区域。4. LPRNet 识别车牌字符从 CTC Loss 到训练数据生成4.1 LPRNet 为什么不用固定长度字符分类车牌字符数不是固定的中国大陆蓝牌是 7 个字符新能源绿牌是 8 个字符如果直接做分类任务就得把所有可能的长度都设为类别浪费参数且无法适应新牌照格式。LPRNet 采用类似 OCR 里常用的 CTCConnectionist Temporal Classification机制网络输出一个时间序列每个时间步预测一个字符分布最后用 CTC 解码去除重复字符和空白符得到最终字符串。这样无论车牌是 7 位还是 8 位都能用同一个模型识别。LPRNet 的网络结构图通常包括一个 CNN 主干类似 VGG 的小型化版本和后面接的 RNN 或者全连接序列层实际训练时我用过纯 CNN 版本的 LPRNet 也能收敛但加上双向 LSTM 后对倾斜字符的鲁棒性更好。CCPD 数据本身包含大量不同角度的车牌足够训练一个识别模型不需要额外加太多人工变形。4.2 训练 LPRNet 的数据准备与代码实现先准备一个dataset.py它负责从原图裁车牌、缩放、转 Tensor并在每个 batch 里返回车牌图片和对应的字符序列索引。下面给出一个可用的训练脚本核心片段import torch import torch.nn as nn from torch.utils.data import Dataset from PIL import Image import glob class CCPDPlateDataset(Dataset): def __init__(self, img_dir, char_map, transformNone, img_w94, img_h24): self.img_paths glob.glob(os.path.join(img_dir, *.jpg)) self.char_map char_map # dict: char - idx self.transform transform self.img_w img_w self.img_h img_h def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img_path self.img_paths[idx] # 从文件名解析四边形坐标并裁切车牌 box, char_str parse_ccpd_filename(img_path) with Image.open(img_path) as im: plate_region im.crop(box) plate_region plate_region.resize((self.img_w, self.img_h), Image.BILINEAR) # 转成灰度或RGBLPRNet通常用灰度 img_tensor torch.from_numpy(np.array(plate_region.convert(L))).float() / 255.0 img_tensor img_tensor.unsqueeze(0) # 单通道 # 将字符序列转为索引列表 label [self.char_map[c] for c in char_str] return img_tensor, torch.tensor(label, dtypetorch.long)这里char_map必须包含所有可能出现的字符包括汉字。CCPD 数据集的汉字只有省份简称但实际部署时建议补充全国所有省份简称否则遇到外省车牌会识别成未知字符。LPRNet 的损失函数是 CTC LossPyTorch 里直接用nn.CTCLoss()但它的输入格式有要求模型输出形状是(T, N, C)T 是时间步数通常等于输入宽度除以 4N 是 batchC 是字符类别总数包括空白符所以要比实际字符数多 1。训练时需要把每个样本的标签长度也传进去CTCLoss 的input_lengths对所有样本都是同一个值即 Ttarget_lengths是每个车牌的实际长度。如果不小心把标签 padding 到统一长度CTC 会计算出错的损失必须传原始长度。4.3 LPRNet 训练的损失函数与收敛判断训练 LPRNet 时我自己常用 Adam 优化器初始学习率 0.001配合余弦退火。CTC Loss 的数值一开始下降很快但到后期会出现震荡这是正常现象。判断模型是否收敛不要只看 loss要看验证集的字符准确率也就是“整个车牌字符串完全正确的比例”。因为 CTC 允许局部字符预测正确但整体顺序错位。我一般训练 60 到 100 个 epoch在验证集准确率连续 10 个 epoch 不提升时保存最佳模型。训练时 batch 大小对 CTC 影响显著batch 太小梯度噪声大建议至少 64。如果显存不足可以把输入图从 94×24 缩到 72×24代价是字符识别率轻微下降。LPRNet 的参数量很小只有几百万训练速度很快普通 GPU 几分钟就能跑完一个 epoch所以完整训练成本并不高。4.4 识别阶段的后处理如何从模型输出得到真实车牌LPRNet 输出经过 CTC 解码后还需要做一步“去除连续重复字符”的操作。比如模型预测序列是“京A A 5 6 7 8”CTC 解码时会合并连续重复的“A A”成一个“A”但要注意“京”和“A”中间不应该有重复合并的问题。实际代码里使用torch.argmax取每个时间步概率最高的字符索引然后按 CTC 规则去掉空白符和相邻重复字符。不过这里有个隐藏陷阱车牌中有数字“0”和字母“O”字符集里两者都存在时模型经常混淆。解决办法是训练结束后统计混淆矩阵把“0”和“O”的类别标签合并成一个或者在后处理阶段根据上下文规则替换。我常用的规则是第二位省份简称后的一位不可能是数字 0所以如果模型第二位输出“0”就改成“O”。这类规则虽然土但在实际工程里比重新训练模型更省事。5. 车牌检测 识别联调从两个独立模型到一条可用的流水线5.1 串行调用的接口设计与时序检测与识别两段式最常见的问题是检测框和识别模型输入之间的坐标映射。如果检测输出的是一个轴对齐矩形框直接把框裁下来缩放后送给 LPRNet 即可。但需要注意CCPD 里的车牌有些是倾斜的YOLOv5 的外接矩形会把背景也包进去此时识别性能会下降。一个成熟方案是把 YOLOv5 的检测头输出改为旋转框yolov5_obb 分支或者在后处理里对裁切图做透视矫正。不过透视矫正会增加计算量对于边界场景才需要。我通常的做法是先用普通矩形框跑通统计识别错误的样本中有多少是因为倾斜导致字符畸变如果比例超过 10% 再引入旋转框检测。联调时的接口最简单的方式是用 Python 内存传递检测模型输出[x1, y1, x2, y2, conf, cls]识别模型接收裁剪图返回字符串。但在实际部署时两个模型往往不在同一进程比如检测跑在 GPU 服务上识别跑在 CPU 设备上这时就需要定义好协议。5.2 树莓派或 RK3568 部署时的量化与推理优化把 YOLOv5 和 LPRNet 部署到边缘设备时常见瓶颈是 YOLOv5 的推理时间。RK3568 这类 NPU 对 YOLOv5 的量化支持比较好但需要把模型转成 RKNN 格式。转换时最容易翻车的是自定义算子比如 YOLOv5 的 Focus 层。新版本 YOLOv5 已经默认用 Conv 替代 Focus但如果你拉的是旧分支需要先升级到较新版本再导出。树莓派 4B 没有 NPU只能靠 CPU 推理建议把 YOLOv5s 转成 ONNX再用 OpenVINO 或 NCNN 推理。实际经验是树莓派 4B 上 YOLOv5s 用 NCNN fp16 推理640 输入大约 2.5 到 3 秒一帧如果不做任何优化几乎不可用。一个折中方案是把输入尺寸降到 320并把 NMS 的候选框数量调低能压到 1 秒左右。如果你做的是停车场的静态图片分析这个速度能接受如果是实时视频流就得考虑换更强的设备或改用 YOLOv5n。LPRNet 本身很小CPU 推理一帧只有几毫秒基本不构成瓶颈。5.3 整条流水线的精度评估与阈值选择联调后的整体准确率并不是两个模型精度的简单乘积因为检测框的质量直接影响识别输入。一个常见的坑是检测模型置信度阈值设得低虽然召回率高但很多包含背景的“假车牌框”被送进识别模型LPRNet 会把背景纹理识别成乱七八糟的字符造成大量误报。解决思路是给识别模型加一个置信度分数——LPRNet 可以输出 CTC 解码路径的概率分数如果分数低于某个阈值例如 0.6视为“不是车牌”而丢弃。这个阈值的选择要在验证集上画 PR 曲线。我通常会同时保存检测框的置信度和识别置信度最后取两者的加权乘积作为最终置信度过滤掉低质量结果。5.4 避坑与常见问题排查现象 1训练 YOLOv5 时 mAP 一直为 0检查标签发现许多 txt 文件为空。原因是 CCPD 文件名解析出现错误某些图片的文件名里坐标段格式不一致例如有负坐标或超出图像范围脚本没有过滤。解决在解析脚本里加上坐标裁剪保证xmin 0且xmax img_w如果解析失败就跳过该文件。现象 2LPRNet 训练 loss 正常下降但验证集准确率始终为 0。大概率是字符映射表里多了空白符的索引冲突。CTCLoss 的类别数C需要比字符数多 1并且模型输出层的大小是len(char_map) 1如果输出层少了一个维度CTC 解码会错位。检查方式打印一次模型输出的尺寸和 char_map 长度。现象 3检测模型对蓝色车牌识别很好对新能源绿色车牌几乎全漏。CCPD 数据集中绿色车牌占比低导致模型拟合不足。解决在训练数据中做类别平衡把绿色车牌的图像复制几份做 mosaic 增强或单独增大绿色车牌的采样权重。不要指望 mAP 能自动覆盖这类长尾。现象 4部署到 RK3568 后YOLOv5 模型输出结果和 PC 上完全不一致。首先检查是否启用了错误的归一化。RKNN 转换时需要把 YOLOv5 的输入归一化参数与转换工具保持一致有些工具默认输入范围是 0-255而 PyTorch 里是 0-1。转模型时必须在转换脚本里明确设置mean_values和std_values否则输出概率分布完全错乱。现象 5识别模型在图像上出现“京A”和“京A”之间的空格字符导致输出有横杠或乱码。这是 CTC 的空白符没有被正确过滤。写后处理时要注意“相邻重复字符”的合并不能跨越空白符必须先按索引过滤掉空白符再合并重复字符顺序反了就会出错。6. 进阶用 CCPD 补数据与外扩视角把准确率从 90% 推到 97% 的实用技巧当检测和识别都能跑通、整体准确率稳定在 90% 左右后想要继续提升最划算的投入是数据外扩和坏样本挖掘而不是继续调模型结构。我习惯的做法是先收集 1000 张实际环境里拍到的真实车牌图片不标注直接用现有检测模型自动标注再人工快速校验把正确标注的并入训练集。这一步能显著调整 CCPD 的“停车场正视角”分布让模型适应你部署现场的光线和角度。对外扩图片的标注要注意如果原图是傍晚或逆光可以先做一次 CLAHE 对比度增强再送检测否则自动标注的框可能偏大或漏检。对于识别模型外扩时要用检测模型得到的框而不是 CCPD 原标注因为实际部署时检测框已经有误差识别模型必须见过“不那么完美”的车牌裁剪图。你可以专门把检测框扩大 5 到 10 像素再裁训练样本让识别模型学会忽略车牌边缘的螺丝和背景。另一个非常有效的技巧是把 CCPD 里的不同图片按车牌字符随机拼贴。比如把“京A”和“沪B”的车牌区域抠出来互换背景或互相拼接制造出新字符组合。CCPD 数据集中某些字符频率不平衡数字 1 和 7 出现远多于字母 Z直接训练会让模型对生僻字符识别率低。拼贴生成可以平衡字符分布但生成时要注意透视和光照匹配做不对反而引入噪声。更简单的替代方案是使用字符级数据增强对裁切后的车牌图做随机仿射变换、亮度抖动和模糊也能缓解不平衡。我的经验是先做坏样本筛选把验证集中识别错误的图片单独列出来统计错误字符的分布找到占比最高的三类错误然后针对性地补数据或加后处理规则而不是盲目扩大数据集。例如如果“0”和“O”错误占多数就在后处理中加上下文规则或者把这两个类合并如果“苏”和“浙”混淆那大概率是小角度模糊导致需要重点补充这两个省份的样本。最后可以尝试把 YOLOv5 的检测输出接入一个轻量级车牌矫正模块比如用 OpenCV 的minAreaRect包围盒找到最小外接矩形再做仿射变换转正。这一步在推理时只增加几十毫秒但能让 LPRNet 的准确率提升 2 到 3 个百分点尤其是对斜停车辆的车牌。我在实际项目中遇到过一个问题摄像头安装在道闸侧面车牌在画面里是一个梯形直接裁剪后字符拉得很长LPRNet 几乎不可用。加了四点透视矫正后识别率从 82% 直接跳到 96%。这就是标准的“检测框是四边形但 YOLOv5 输出是矩形”带来的收益空间。所以如果你手里的摄像头视角比较刁钻不要一开始就去改网络结构先试试这个后处理技巧。我自己每次做车牌项目都会先把矫正这一步放在“必须实现”清单里而不是“可选优化”。它能救回很多看似无解的样本。希望这些经验对你有所帮助能从 CCPD 出发快速搭出一套可靠的车牌检测识别系统。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站