简介:本资源面向计算机视觉方向的毕业设计、课程设计及学科竞赛参与者,提供一套基于CNN与YOLOv5的车牌检测与识别完整工程,数据集采用CCPD官方数据集,可帮助读者快速搭建车牌识别实验环境并完成项目复现。压缩包共10个文件,约44.33MB,包含Python脚本、模型权重文件、YAML配置、Keras模型文件、Jupyter Notebook及README说明文档,覆盖训练、检测与识别全流程,便于直接运行与二次开发。目前已有78人学习下载,适合具备一定深度学习基础的学习者参考。资源内代码经过测试运行,功能完整,可借鉴其设计报告与工程结构,也可在此基础上扩展出更多功能,如多车牌场景、不同字符集识别等。下载后建议先阅读说明文件,按目录结构逐步理解模型加载与推理逻辑,适合用于项目立项、实训练手及技术学习参考。
1. 从一张模糊卡口图说起:CNN 加 YOLOv5 的车牌检测识别到底怎么落地
夜里十一点,卡口相机拍到一张车尾图,车牌区域只占整幅画面的百分之三,还带运动模糊和车灯眩光。传统做法是先做边缘检测再套字符分割模板,这种图基本直接翻车。换成基于 CNN 和 YOLOv5 的车牌检测识别方案后,流程变成两段:YOLOv5 负责在整图里把车牌框出来,CNN 分类网络负责把框里的字符逐个认出来。这套组合在 CCPD 官方数据集上能跑出可用的精度,也是目前毕设、课设、实训和大作业里最常见的技术路线。
CCPD 全称 Chinese City Parking Dataset,是国内车牌识别方向绕不开的公开数据集,覆盖多种天气、光照、倾斜角度和车牌类型。它省掉了自己标注的苦力活,但原始标注格式和 YOLOv5 要求的格式不一致,这是第一个要迈的坎。这篇文章面向想把这套方案真正跑起来的人:从数据格式转换、YOLOv5 训练调参,到 CNN 字符识别头、推理串联和部署踩坑,每一步都给可复现的命令和参数。读完你应该能自己搭出一条从 CCPD 到可推理模型的完整链路,而不是停在「跑通了 demo」这一步。
2. CCPD 数据集拆解与 YOLOv5 格式转换:标注解析和四个边界坑
2.1 CCPD 的目录结构和文件名编码逻辑
CCPD 不是常见的 images + labels 双目录结构,它把标注信息直接编码在文件名里。一个典型文件名长这样:
025-95_113-226&469_448&550-444&551_453&553_449&555_446&557-0_0_22_27_27_27_24-66-88.jpg按短横线切分后,各字段含义如下:
| 字段位置 | 含义 | 示例值 |
|---|---|---|
| 第 1 段 | 区域编号 | 025 |
| 第 2 段 | 水平倾斜角与垂直倾斜角 | 95_113 |
| 第 3 段 | 车牌边界框左上与右下坐标 | 226&469_448&550 |
| 第 4 段 | 四个角点坐标 | 444&551_453&553_449&555_446&557 |
| 第 5 段 | 亮度与模糊度 | 0_0 |
| 第 6 段 | 车牌颜色 | 22 |
| 第 7 段 | 字符索引序列 | 27_27_27_24 |
| 第 8 段 | 省份与城市编码 | 66-88 |
真正做检测只需要第 3 段的边界框坐标,做识别需要第 7 段加第 8 段还原出真实字符。很多人第一次拿到 CCPD 会直接去找 label 文件,找不到就以为数据集下错了,其实标注全在文件名里。理解这套编码是后面所有转换脚本的前提。
2.2 把文件名解析成 YOLO 训练标签
YOLOv5 要求每个图像对应一个同名 txt,每行格式为class cx cy w h,坐标全部归一化到 0 到 1。下面这个脚本把 CCPD 文件名直接转成 YOLO 标签:
import os import cv2 # CCPD 文件名第 3 段是边界框,格式为 x1&y1_x2&y2 def parse_bbox_from_name(fname): stem = os.path.splitext(fname)[0] parts = stem.split('-') # 第 3 段索引为 2,形如 226&469_448&550 box_str = parts[2] left_top, right_bottom = box_str.split('_') x1, y1 = map(int, left_top.split('&')) x2, y2 = map(int, right_bottom.split('&')) return x1, y1, x2, y2 def convert_to_yolo(img_dir, out_dir, img_w=720, img_h=1160): os.makedirs(out_dir, exist_ok=True) for fname in os.listdir(img_dir): if not fname.lower().endswith('.jpg'): continue x1, y1, x2, y2 = parse_bbox_from_name(fname) # 归一化中心点与宽高 cx = (x1 + x2) / 2.0 / img_w cy = (y1 + y2) / 2.0 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h # 单类别车牌,class id 固定为 0 line = f"0 {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n" txt_name = os.path.splitext(fname)[0] + '.txt' with open(os.path.join(out_dir, txt_name), 'w') as f: f.write(line) if __name__ == '__main__': convert_to_yolo('./ccpd/images', './ccpd/labels')逻辑说明:parse_bbox_from_name只取文件名第 3 段,因为 YOLOv5 检测只需要矩形框,不需要四个角点。归一化时用的img_w和img_h必须和实际图像尺寸一致,CCPD 图像尺寸并不完全统一,常见为 720x1160,但部分子集有差异。参数上,class id固定为 0 表示只有「车牌」一类;如果你还想区分蓝牌、绿牌,可以把第 6 段颜色字段映射成多个类别,但那样检测头要改类别数。
2.3 划分训练集验证集时最容易忽略的坑
CCPD 官方给的划分是按文件名前缀区分的,比如ccpd_base、ccpd_blur、ccpd_challenge等子集。直接随机打乱全部文件做 train/val split 会有一个隐蔽问题:同一辆车、同一场景的连拍图可能同时进训练和验证,导致验证精度虚高。我一般按子集整体划分,比如 base 和 blur 进训练,challenge 单独做验证,这样验证指标更接近真实卡口场景。
另一个坑是图像和标签必须同名同目录层级。YOLOv5 默认按images/train和labels/train的路径替换规则找标签,如果你把标签放在别的地方,需要在 data yaml 里显式指定labels路径,否则训练时全部标签丢失,loss 直接不降。
2.4 生成 YOLOv5 的 data yaml
转换完标签后,写一个 data yaml 告诉 YOLOv5 去哪找数据:
path: ./ccpd train: images/train val: images/val nc: 1 names: ['plate']nc是类别数,车牌检测只有一类所以是 1。names的顺序必须和标签里的 class id 对应,写反了不影响训练但推理时类别名会错。这个文件放在项目根目录,训练命令里用--data ccpd.yaml引用即可。
3. YOLOv5 车牌检测训练:从 conda 环境到超参数怎么调
3.1 环境搭建与依赖版本选择
YOLOv5 对 PyTorch 和 CUDA 版本比较敏感,版本错配最常见的表现是训练能启动但 GPU 利用率一直是 0,或者直接报 CUDA 相关错误。我一般用 conda 建独立环境,避免和系统里的其他深度学习项目打架:
conda create -n plate python=3.9 -y conda activate plate # 按自己显卡驱动选对应 CUDA 版本的 torch pip install torch==1.13.1 torchvision==0.14.1 --index-url https://download.pytorch.org/whl/cu117 git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt逻辑说明:Python 3.9 是 YOLOv5 各版本兼容性最好的选择,3.11 以上部分依赖会编译失败。torch 版本要和本机 CUDA 驱动匹配,cu117表示 CUDA 11.7,驱动版本不够就往下选cu113。装完用python -c "import torch; print(torch.cuda.is_available())"验证,输出 True 才算环境通了。
3.2 用预训练权重做迁移学习
车牌检测属于小目标密集场景,从零训练收敛慢且容易过拟合。标准做法是加载 COCO 预训练权重做迁移:
python train.py \ --img 640 \ --batch 16 \ --epochs 100 \ --data ccpd.yaml \ --weights yolov5s.pt \ --cfg models/yolov5s.yaml \ --name plate_yolov5s参数说明:--img 640是输入分辨率,车牌在图中占比小,理论上更大分辨率对小目标更友好,但 640 是速度和精度的平衡点,显存不够就降到 512。--batch 16按显存调,8G 显存跑 640 分辨率大概能到 16。--weights yolov5s.pt是官方在 COCO 上训好的权重,迁移学习能省掉大量收敛时间。--epochs 100对 CCPD 这种规模通常够用,看results.csv里 mAP 不再上升就可以提前停。
3.3 三个必调超参数和它们的实际影响
YOLOv5 的超参数文件在data/hyp.scratch.yaml,但车牌场景我一般只动三个:
| 参数 | 默认值 | 车牌场景建议 | 原因 |
|---|---|---|---|
| lr0 | 0.01 | 0.001 | 迁移学习时学习率太大会破坏预训练特征 |
| anchor | 默认 COCO | 用 kmeans 重聚类 | 车牌宽高比和 COCO 目标差异大 |
| mosaic | 1.0 | 0.5 | 马赛克增强对小目标有帮助但过强会引入噪声 |
anchor 重聚类可以用 YOLOv5 自带的脚本:
python utils/autoanchor.py --data ccpd.yaml --img 640它会根据你的数据集重新算 anchor 尺寸,车牌是扁长矩形,默认 anchor 里没有匹配的形状,不重聚类会明显掉点。mosaic 增强调到 0.5 是因为 CCPD 里已经有大量模糊和倾斜样本,再叠加过强的马赛克增强反而让模型学到无关特征。
3.4 训练过程怎么判断有没有跑偏
训练启动后重点看三个指标:box_loss、obj_loss和mAP@0.5。正常情况 box_loss 在前 10 个 epoch 快速下降,obj_loss 缓慢下降,mAP 稳步上升。如果 box_loss 震荡不降,大概率是学习率太大或标签有问题;如果 mAP 一直卡在很低的值,先检查标签路径对不对,再检查 anchor 是否匹配。验证集上的mAP@0.5到 0.95 以上基本可用,低于 0.85 就要回头查数据。
4. CNN 字符识别头:从车牌裁剪到字符序列输出
4.1 为什么识别不用 YOLOv5 而单独上 CNN
YOLOv5 能做检测,但直接让它输出字符序列并不合适。车牌识别本质是序列识别问题,字符之间有固定位置关系,用检测框逐个框字符再分类,遇到字符粘连或模糊就会漏框。更稳的做法是检测框裁出车牌后,用一个 CNN 分类网络做整牌识别,或者用 CNN 提取特征再接 CTC 做序列解码。毕设和课设里最常见的是 CNN 多标签分类:把车牌固定为 7 个字符位,每个位置做一个分类头。
4.2 从 CCPD 文件名还原字符标签
识别训练需要字符级标签,CCPD 文件名第 7 段是字符索引,第 8 段是省市编码。还原逻辑如下:
# CCPD 字符映射表,索引到真实字符 CHARS = ['京', '津', '沪', '渝', '冀', '晋', '辽', '吉', '黑', '苏', '浙', '皖', '闽', '赣', '鲁', '豫', '鄂', '湘', '粤', '桂', '琼', '川', '贵', '云', '藏', '陕', '甘', '青', '宁', '新', '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'] def decode_label(fname): stem = os.path.splitext(fname)[0] parts = stem.split('-') # 第 7 段是字符索引,第 8 段是省市编码 char_ids = list(map(int, parts[6].split('_'))) province_city = parts[7].split('-') # 省市编码需要按 CCPD 官方映射表转成中文,这里简化为索引 label = [CHARS[i] for i in char_ids] return label逻辑说明:CHARS列表的顺序必须和 CCPD 官方定义一致,顺序错了标签全乱。第 7 段通常有 4 到 5 个索引,对应车牌后几位;第 8 段是省市编码,需要额外映射表转成汉字。实际训练时把 7 个字符位拼成固定长度序列,不足补空白符。
4.3 CNN 识别网络的结构和训练配置
识别网络我一般用轻量 CNN 加多分类头,输入是检测框裁出的车牌图,统一 resize 到 94x24:
import torch import torch.nn as nn class PlateCNN(nn.Module): def __init__(self, num_chars=65, seq_len=7): super().__init__() self.seq_len = seq_len self.backbone = nn.Sequential( nn.Conv2d(3, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), ) self.pool = nn.AdaptiveAvgPool2d((1, 1)) # 每个字符位一个分类头 self.heads = nn.ModuleList([ nn.Linear(128, num_chars) for _ in range(seq_len) ]) def forward(self, x): feat = self.backbone(x) feat = self.pool(feat).flatten(1) return [head(feat) for head in self.heads]逻辑说明:backbone 三层卷积负责提特征,AdaptiveAvgPool2d把任意尺寸特征压成固定长度向量,heads是 7 个独立全连接层,每个负责一个字符位的分类。num_chars是字符集大小,包含省份简称、数字和字母,约 65 类。训练时每个头的输出分别算交叉熵再求和,这样每个字符位独立学习,不会互相干扰。
训练配置上,识别网络比检测网络轻,batch 可以开到 64,学习率 0.001,用 Adam 优化器。数据增强只做轻微的亮度和对比度扰动,不要做随机裁剪,因为裁剪会破坏字符位置关系。
4.4 检测和识别怎么串成一条推理链路
推理时先跑 YOLOv5 拿到车牌框,按框裁剪并做透视校正,再送进 CNN 识别:
import cv2 import numpy as np def inference(img_path, det_model, rec_model, device='cuda'): img = cv2.imread(img_path) # YOLOv5 推理,拿到框 results = det_model(img) boxes = results.xyxy[0].cpu().numpy() plates = [] for box in boxes: x1, y1, x2, y2 = map(int, box[:4]) crop = img[y1:y2, x1:x2] # 统一尺寸送识别网络 crop = cv2.resize(crop, (94, 24)) crop = crop.transpose(2, 0, 1)[None] / 255.0 tensor = torch.from_numpy(crop).float().to(device) with torch.no_grad(): logits = rec_model(tensor) chars = [logit.argmax(1).item() for logit in logits] plates.append((box[:4], chars)) return plates逻辑说明:results.xyxy[0]是 YOLOv5 输出的检测框,格式为左上右下加置信度。裁剪后 resize 到识别网络固定输入尺寸,归一化到 0 到 1。识别网络输出 7 个 logits,每个取 argmax 得到字符索引,再查表还原成真实字符。整条链路的关键是检测框要准,框偏了识别必错,所以检测和识别要分开评估,不要只看端到端结果。
5. 训练和部署里最容易翻车的五个地方
5.1 现象:训练 loss 正常但验证 mAP 极低
原因:标签路径配置错误,YOLOv5 找不到标签文件,把所有框当成背景在学。解决:检查 data yaml 里的train和val路径,确认labels目录和images目录层级对应,用python utils/general.py里的检查函数验证标签数量。
5.2 现象:识别网络在验证集上准确率高,实际图片全错
原因:训练时用的字符映射表和推理时不一致,或者 CCPD 文件名解析时省市编码映射错位。解决:把训练和推理的字符映射表抽成同一个模块,两边 import 同一份,不要各写一份。
5.3 现象:GPU 显存够但训练速度极慢
原因:--workers设得太小,数据加载成了瓶颈;或者图像尺寸设得过大导致每步计算量暴涨。解决:--workers设成 CPU 核心数,--img从 640 开始试,不要一上来就 1280。
5.4 现象:检测框位置对但识别结果字符顺序颠倒
原因:车牌有正向和反向,CCPD 里包含大量倾斜和翻转样本,CNN 没有方向不变性。解决:在检测后加一步方向判断,或者训练时加入翻转增强让模型学到方向特征。
5.5 现象:模型在本地跑得好,换一台机器推理结果全乱
原因:OpenCV 读图默认 BGR,训练时如果用的是 PIL 读的 RGB,通道顺序不一致。解决:统一读图方式,训练和推理都用同一种,或者在推理前显式做 BGR 到 RGB 转换。
6. 把检测和识别合成一个可部署模型:ONNX 导出与量化提速
训练完两个模型后,部署时最直接的做法是分别导出 ONNX 再串联。YOLOv5 自带导出脚本:
python export.py --weights runs/train/plate_yolov5s/weights/best.pt --include onnx --img 640识别网络手动导出:
torch.onnx.export( rec_model, torch.randn(1, 3, 24, 94).to(device), 'plate_rec.onnx', input_names=['input'], output_names=[f'char_{i}' for i in range(7)], dynamic_axes={'input': {0: 'batch'}}, opset_version=11 )导出后可以用 onnxruntime 做推理,速度比 PyTorch 原生推理快不少,尤其是在没有 GPU 的部署环境里。如果还要进一步压速度,可以对识别网络做动态量化:
from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( 'plate_rec.onnx', 'plate_rec_int8.onnx', weight_type=QuantType.QUInt8 )量化后模型体积大概降到原来的四分之一,CPU 推理速度提升明显,精度损失通常在 1 个百分点以内。检测模型量化要谨慎,YOLOv5 的卷积层对量化比较敏感,建议只量化识别头。
验证导出是否正确,用一张测试图跑一遍 ONNX 和 PyTorch 的结果对比,输出差异应该在 1e-3 以内。如果差异过大,检查 opset 版本和输入尺寸是否和导出时一致。
我自己的习惯是:每次改完模型结构或预处理,先导出 ONNX 跑一张图对齐数值,再上批量测试。这个习惯帮我省过好几次「本地好好的、部署就崩」的后悔药。车牌检测识别这条链路不算复杂,但检测和识别两个环节的预处理必须严格对齐,任何一边的归一化或通道顺序变了,端到端结果就会崩。希望帮到你。
本文还有配套的精品资源,点击获取