简介:这份资源面向计算机、电子信息、数学等专业的大学生及算法初学者,提供一套基于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_ok=True) 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 = f"0 {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 为 1,names 为 ['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(blank=0, reduction='mean') optimizer = torch.optim.Adam(model.parameters(), lr=1e-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), dtype=torch.long) loss = ctc_loss(preds, labels, input_lens, label_lens) optimizer.zero_grad() loss.backward() optimizer.step()参数说明:blank=0对应字符集里的 blank;preds的形状是时间步在前、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(blank=0)和字符集定义,确保 blank 索引一致。
现象四:夜间车牌识别率骤降。原因:训练集里夜间样本少,或者预处理没有做暗光增强。 解决:对夜间样本过采样,训练时加随机亮度对比度扰动,推理时加 CLAHE。
现象五:树莓派 5 上推理速度只有 2 FPS。原因:模型没量化,输入尺寸太大,或者用了 CPU 推理。 解决:导出 ONNX 后用 ONNX Runtime 或 OpenVINO,输入降到 416,开启 INT8 量化。
6. 把模型塞进边缘设备:导出、量化和一个提速技巧
训练完的模型要落地,第一步是导出。YOLOv5 用export.py导出 ONNX,LPRNet 手动写导出脚本。导出时注意 opset 版本,YOLOv5 建议 12,LPRNet 建议 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(maxsize=10)做缓冲,检测线程 put,识别线程 get,注意加锁保护共享的模型实例。
我自己踩过最深的坑是导出 ONNX 后忘了把 LPRNet 的输入尺寸写死,导致动态轴推理时 CTC 解码长度对不上,识别结果错位。后来养成习惯,导出后一定用固定尺寸跑一遍端到端测试,确认检测框和识别结果能对上。希望帮到你。
本文还有配套的精品资源,点击获取