简介:项目压缩包是一套基于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-154&383_386&473-386&473_177&454_154&383_363&402-0_0_22_27_27_33_16-37-15.jpg”。拆开看:第一段是车牌类型和区域代号;第二段是亮度与对比度特征;第三段是车牌的四个顶点坐标(格式为 x1&y1_x2&y2_x3&y3_x4&y4,注意不是常见的左上右下两点框);第四段是车牌字符标签,按顺序排列,每个数字对应一个字符类别;最后是车牌宽度和高度。这种命名方式意味着检测标注不需要额外的 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_ok=True) # CCPD 文件名示例: 025-95_113-154&383_386&473-...-37-15.jpg # 第三段是四个顶点坐标,第四段是字符标签,需要按需求取舍 def parse_ccpd_filename(filename): base = os.path.basename(filename).replace(".jpg", "") parts = base.split("-") # parts[2] 是四边形顶点,形如 154&383_386&473_...(四个点用下划线分隔) 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 f"0 {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_ok=True) 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,太小边缘字符容易缺,太大背景噪声变多。裁完之后每个字符类别需要映射成一个数字 ID,CCPD 的字符标签字段覆盖了省份简称缩写、字母和数字,需要建立一张从字符到索引的映射表。映射表千万别手工敲,直接从训练集里统计出现的字符集合自动生成,否则漏掉某个汉字会让训练时索引越界。
3. 用 YOLOv5 训练车牌检测器:网络结构选择与超参数调整
3.1 YOLOv5s 还是 YOLOv5m:先看部署目标再定网络结构图
YOLOv5 有 s、m、l、x 四个主要版本,区别在网络的深度和宽度倍数。车牌检测目标小、类别单一,不需要很大的模型容量。如果后续要部署到树莓派或 RK3568 这类边缘设备,优先选 YOLOv5s;如果是在办公电脑或服务器上跑,YOLOv5m 会带来更稳的召回率。我在项目里通常先用 YOLOv5s 跑通基线,确认检测精度不够再加 m。车牌这个场景下,s 和 m 的 mAP 差距通常不到 2 个百分点,但推理速度差距很大,所以不要盲目追求大模型。网络结构图里真正需要关注的是检测头输出层的 stride:YOLOv5 有三个检测层,分别对应 8、16、32 倍下采样,车牌这种中等尺寸目标主要靠中间那个 stride 16 的检测层,如果发现小角度车牌漏检,可以额外关注 stride 8 的特征层是否需要加强。
3.2 YOLOv5 训练自己的数据集:流程与关键参数
训练前要准备好数据集目录结构,并写一个数据集 yaml 文件。YOLOv5 的 data yaml 里只需要三行核心内容:训练图片路径、验证图片路径、类别数量与名称。注意 CCPD 只有一类,所以 nc 填 1,names 写成['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 设为 8,img 降到 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改成 0,hsv_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 里常用的 CTC(Connectionist 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, transform=None, img_w=94, img_h=24): 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) # 转成灰度或RGB,LPRNet通常用灰度 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, dtype=torch.long)这里char_map必须包含所有可能出现的字符,包括汉字。CCPD 数据集的汉字只有省份简称,但实际部署时建议补充全国所有省份简称,否则遇到外省车牌会识别成未知字符。LPRNet 的损失函数是 CTC Loss,PyTorch 里直接用nn.CTCLoss(),但它的输入格式有要求:模型输出形状是(T, N, C),T 是时间步数(通常等于输入宽度除以 4),N 是 batch,C 是字符类别总数(包括空白符,所以要比实际字符数多 1)。训练时需要把每个样本的标签长度也传进去,CTCLoss 的input_lengths对所有样本都是同一个值(即 T),target_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,如果解析失败就跳过该文件。
现象 2:LPRNet 训练 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 出发,快速搭出一套可靠的车牌检测识别系统。
本文还有配套的精品资源,点击获取