2026/9/28 5:04:27

基于CCPD数据集与CNN+YOLOv5的车牌检测识别实战

基于CCPD数据集与CNN+YOLOv5的车牌检测识别实战 简介这份资源面向计算机视觉方向的毕业设计、课程设计及学科竞赛参与者提供一套基于CNN与YOLOv5的车牌检测与识别完整工程数据集采用CCPD官方数据可解决车牌定位与字符识别两类核心任务适合具备一定深度学习基础、希望快速复现或二次开发的学习者。压缩包共10个文件约44.33MB包含Python脚本、模型权重文件、YAML配置、Keras与TensorFlow模型文件、Jupyter Notebook及说明文档覆盖训练、推理与模型加载等环节。已有78人学习关注。资源经过实际运行测试功能完整可复现出与作者一致的项目效果设计报告亦可借鉴读者可据此理解YOLOv5检测与CNN识别的衔接方式掌握数据配置、模型训练与推理流程并在此基础上扩展出更多功能适用于项目立项、实训与竞赛等场景。1. 车牌检测与识别从 CCPD 数据集到 CNNYOLOv5 的完整落地路径车牌检测和识别是计算机视觉里少有的「模型结构清晰、数据集现成、评价指标明确」的方向CCPD 数据集的出现让这件事的门槛降到了个人开发者也能跑通的程度。CCPD 全称 Chinese City Parking Dataset采集自真实停车场场景包含超过 25 万张带标注的车辆图片标注信息直接编码在文件名里涵盖车牌位置、四个角点坐标、车牌号码和颜色。这意味着你不需要自己写标注工具下载解压就能开始训练。这套方案的核心思路是两阶段YOLOv5 负责从整张图中定位车牌区域CNN 负责对裁剪出的车牌做字符识别。YOLOv5 在目标检测上的速度和精度平衡已经被大量工程验证过而 CNN 做字符分类是深度学习最成熟的应用之一。两者组合既能保证检测框的准确性又能逐字符输出车牌号。适合做毕设、课设、实训项目的同学也适合想快速验证车牌识别 pipeline 的工程师。下面从数据准备、检测训练、识别模型搭建到部署排错一步步拆开讲。2. CCPD 数据集处理与 YOLOv5 检测器训练2.1 CCPD 数据集的目录结构与标注解析CCPD 数据集下载解压后你会看到ccpd_base、ccpd_blur、ccpd_green、ccpd_np等若干子目录每个子目录对应不同拍摄条件。以ccpd_base为例文件名格式是这样的025-95_113-184478_446521-444521_438478_184478_190521-0_0_22_27_27_33_16-66-88.jpg每一段用-分隔含义如下字段位置含义示例值第1段区域编号025第2段倾斜角度95_113第3段边界框坐标184478_446521第4段四个角点坐标444521_438478_184478_190521第5段车牌号码索引0_0_22_27_27_33_16第6段亮度66第7段模糊度88其中第3段184478_446521表示左上角和右下角的 x、y 坐标这是 YOLO 训练需要的边界框信息。第5段的数字是字符在省份、字母、数字字典中的索引需要映射回真实字符。常见做法是写一个解析脚本把文件名拆解后生成 YOLO 格式的标注文件。YOLO 要求的标注格式是class_id x_center y_center width height所有值归一化到 0~1 之间。import os import cv2 def parse_ccpd_filename(filename): 解析 CCPD 文件名返回边界框和车牌号 name filename.split(.)[0] parts name.split(-) # 第3段是边界框 bbox_str parts[2] x1, y1 map(int, bbox_str.split(_)[0].split()) x2, y2 map(int, bbox_str.split(_)[1].split()) # 第5段是车牌字符索引 char_indices list(map(int, parts[4].split(_))) return (x1, y1, x2, y2), char_indices def convert_to_yolo(img_dir, label_dir): 将 CCPD 标注转为 YOLO 格式 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) h, w img.shape[:2] (x1, y1, x2, y2), _ parse_ccpd_filename(fname) # 归一化中心点和宽高 cx (x1 x2) / 2.0 / w cy (y1 y2) / 2.0 / h bw (x2 - x1) / w bh (y2 - y1) / h label_path os.path.join(label_dir, fname.replace(.jpg, .txt)) with open(label_path, w) as f: f.write(f0 {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n)这段代码的逻辑很直接读图拿到宽高解析文件名拿到边界框然后做归一化写入 txt。参数方面class_id固定为 0因为只检测车牌这一类。注意 CCPD 有些图片的边界框可能超出图像范围写入前最好做一次 clip 操作把坐标限制在[0, 1]区间内否则训练时 YOLOv5 的 dataloader 会报错。2.2 YOLOv5 环境配置与训练参数设置YOLOv5 的环境配置用 conda 最省心。我一般会单独建一个环境避免和系统里的其他包冲突conda create -n yolov5 python3.8 conda activate yolov5 git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt装完之后验证一下 GPU 是否可用import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果输出True和显卡型号说明环境没问题。接下来准备数据配置文件ccpd.yamlpath: /data/ccpd train: images/train val: images/val nc: 1 names: [plate]训练命令用 YOLOv5s 预训练权重做迁移学习python train.py \ --img 640 \ --batch 16 \ --epochs 100 \ --data ccpd.yaml \ --weights yolov5s.pt \ --project runs/train \ --name ccpd_exp参数说明--img 640是输入分辨率CCPD 图片普遍较大640 能在精度和显存之间取得平衡--batch 16在 8GB 显存下比较稳妥如果显存不够降到 8--epochs 100对 CCPD 这种量级的数据集通常够用观察 mAP 曲线如果 80 轮后还在涨可以加到 150。--weights yolov5s.pt加载 COCO 预训练权重比从头训练收敛快很多这是血泪经验——不加载预训练权重前 20 轮基本在瞎跑。训练过程中重点关注mAP0.5和mAP0.5:0.95两个指标。车牌检测任务里mAP0.5通常能到 0.98 以上如果低于 0.95大概率是标注转换有问题回头检查归一化坐标是否写反了 x 和 y。3. CNN 字符识别模型从车牌裁剪到序列输出3.1 车牌字符集定义与 CNN 网络结构设计CCPD 数据集的车牌字符集包括31 个省份简称、24 个英文字母不含 I 和 O、10 个数字总共 65 个类别。标准车牌是 7 位字符第一位是省份第二位是字母后面五位是字母或数字混合。CNN 识别模型的设计思路是输入一张裁剪好的车牌图片输出 7 个字符的预测结果。常见做法有两种一种是训练 7 个独立的分类头每个头负责一个位置的字符分类另一种是训练一个序列模型用 CTC 损失做端到端输出。对于毕设和课设场景7 个分类头的方案更容易实现和调试。网络结构用经典的卷积堆叠加全连接import torch import torch.nn as nn class PlateCNN(nn.Module): def __init__(self, num_chars65, num_positions7): super().__init__() self.num_positions num_positions # 卷积特征提取 self.features nn.Sequential( nn.Conv2d(3, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(128, 256, 3, padding1), nn.ReLU(), nn.AdaptiveAvgPool2d((4, 8)) ) # 7 个分类头每个头输出 65 类 self.heads nn.ModuleList([ nn.Sequential( nn.Flatten(), nn.Linear(256 * 4 * 8, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, num_chars) ) for _ in range(num_positions) ]) def forward(self, x): feat self.features(x) # 每个头独立输出一个位置的字符 logits outputs [head(feat) for head in self.heads] return torch.stack(outputs, dim1) # (B, 7, 65)输入图片统一 resize 到(3, 32, 128)这是车牌的长宽比决定的。卷积部分用了 4 层每层后接 ReLU 和池化最后用AdaptiveAvgPool2d固定到(4, 8)这样全连接层的输入维度就是确定的。7 个分类头结构相同但参数独立每个头负责一个位置的字符预测。参数方面Dropout(0.3)是为了防止过拟合CCPD 数据量大但字符分布不均衡某些省份简称出现频率远高于其他。如果发现验证集准确率远低于训练集把 Dropout 提到 0.5 试试。3.2 训练 CNN 识别模型与数据增强策略训练 CNN 之前需要先从 CCPD 原图中裁剪出车牌区域。可以用 YOLOv5 训练好的检测器做推理也可以用 CCPD 文件名里的边界框直接裁剪。后者更简单适合快速验证import cv2 import numpy as np from torch.utils.data import Dataset class PlateRecDataset(Dataset): def __init__(self, img_dir, transformNone): self.img_dir img_dir self.files [f for f in os.listdir(img_dir) if f.endswith(.jpg)] self.transform transform def __len__(self): return len(self.files) def __getitem__(self, idx): fname self.files[idx] img cv2.imread(os.path.join(self.img_dir, fname)) # 从文件名解析边界框 parts fname.split(.)[0].split(-) bbox parts[2] x1, y1 map(int, bbox.split(_)[0].split()) x2, y2 map(int, bbox.split(_)[1].split()) plate img[y1:y2, x1:x2] plate cv2.resize(plate, (128, 32)) plate cv2.cvtColor(plate, cv2.COLOR_BGR2RGB) # 解析字符标签 char_indices list(map(int, parts[4].split(_))) if self.transform: plate self.transform(plate) return plate, torch.tensor(char_indices, dtypetorch.long)数据增强对识别模型很关键。车牌图片的干扰主要来自光照变化、模糊和倾斜。我一般会加以下几种增强随机亮度对比度调整、轻微高斯模糊、小角度旋转。注意旋转角度不要超过 5 度否则字符变形太严重反而影响训练。训练循环用标准的交叉熵损失7 个位置分别计算 loss 再求和criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.5) for epoch in range(50): model.train() for imgs, labels in train_loader: imgs, labels imgs.cuda(), labels.cuda() preds model(imgs) # (B, 7, 65) loss sum(criterion(preds[:, i, :], labels[:, i]) for i in range(7)) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()学习率初始设1e-3每 20 轮降一半。如果 loss 在前几轮就降到很低但验证准确率上不去检查一下数据增强是不是太弱导致过拟合。正常情况下CCPD 训练 30 轮左右7 位字符全对的准确率能到 90% 以上。4. 检测与识别串联完整推理 pipeline 搭建4.1 YOLOv5 推理结果解析与车牌区域裁剪YOLOv5 的推理输出是(x1, y1, x2, y2, confidence, class)格式的数组。拿到检测框后需要做两件事过滤低置信度的框以及处理重叠框。YOLOv5 自带 NMS但默认阈值是 0.45车牌检测场景下可以调到 0.3因为同一张图里一般只有一个车牌不需要保留太多候选框。import torch def detect_plate(model, img_path, conf_thres0.5, iou_thres0.3): 用 YOLOv5 检测车牌返回裁剪后的车牌图片列表 img cv2.imread(img_path) results model(img, size640) detections results.pandas().xyxy[0] detections detections[detections[confidence] conf_thres] plates [] for _, row in detections.iterrows(): x1, y1, x2, y2 int(row[xmin]), int(row[ymin]), int(row[xmax]), int(row[ymax]) # 适当外扩避免字符被切掉 pad 5 x1 max(0, x1 - pad) y1 max(0, y1 - pad) x2 min(img.shape[1], x2 pad) y2 min(img.shape[0], y2 pad) plate img[y1:y2, x1:x2] plates.append(plate) return plates这里有个容易翻车的点YOLOv5 输出的坐标是浮点数直接取整可能把边缘字符切掉一两个像素。我一般会外扩 5 个像素这个值可以根据实际效果微调。如果发现识别结果经常缺字把 pad 加到 8 或 10 试试。4.2 字符解码与结果后处理CNN 输出的是每个位置的字符索引需要映射回真实字符。字典的定义要和 CCPD 的索引顺序一致# CCPD 字符字典顺序必须和数据集一致 CHARS [皖, 京, 津, 渝, 沪, 冀, 晋, 辽, 吉, 黑, 苏, 浙, 鄂, 湘, 粤, 桂, 琼, 川, 贵, 云, 藏, 陕, 甘, 青, 宁, 新, A, B, C, D, E, F, G, H, J, K, L, M, N, P, Q, R, S, T, U, V, W, X, Y, Z, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] def decode_plate(preds): 将 CNN 输出解码为车牌字符串 indices preds.argmax(dim-1) # (7,) plate_str .join(CHARS[i] for i in indices) return plate_str后处理还包括一些规则校验第一位必须是省份简称第二位必须是字母后面五位是字母或数字。如果解码结果不符合这个规则说明模型在某些位置预测错了可以输出置信度最低的位置供人工复核。完整的推理流程是读图 → YOLOv5 检测 → 裁剪车牌 → resize 到 (128, 32) → CNN 识别 → 解码输出。整个 pipeline 在 GPU 上单张图耗时大约 30~50ms满足实时性要求。5. 避坑与排查车牌检测识别中常见的 5 个翻车现场5.1 训练 loss 不下降或震荡严重现象YOLOv5 训练前 10 轮 loss 一直在高位震荡mAP 接近 0。原因最常见的是标注文件格式错误。CCPD 的边界框是x1y1_x2y2格式如果解析时把 x 和 y 搞反了归一化后的坐标就完全错了。另一个原因是学习率设得太大YOLOv5 默认 lr 是 0.01如果数据集小可以降到 0.001。解决先用python train.py --img 640 --batch 4 --epochs 3跑一个迷你训练看 loss 是否在 3 轮内明显下降。如果没降用cv2.rectangle把标注框画到原图上肉眼检查这一步能省掉后面几小时的无效训练。5.2 检测框位置正确但识别结果乱码现象YOLOv5 检测框很准但 CNN 输出的车牌号完全不对。原因字符字典的顺序和 CCPD 数据集不一致。CCPD 的字符索引是固定的省份简称在前字母在后数字最后。如果自己重新排列了字典顺序模型学到的索引和字典对不上输出就是乱的。解决直接用 CCPD 官方提供的字典顺序不要自己改。验证方法是拿一张训练集里的图片用文件名里的字符索引去查字典看是否能拼出合理的车牌号。5.3 验证集准确率远低于训练集现象训练集准确率 98%验证集只有 70%。原因过拟合。CCPD 的ccpd_base子集里某些省份的车牌出现频率极高模型会偏向预测高频省份。另外数据增强太弱也会导致模型记住训练样本。解决增强数据增强加随机裁剪、颜色抖动、小角度旋转。同时在分类头里加 Dropout从 0.3 提到 0.5。如果还不行检查训练集和验证集的划分是否随机不要按文件名排序后直接切分那样会导致同一辆车的图片同时出现在训练集和验证集里。5.4 推理时 GPU 显存溢出现象训练正常推理时CUDA out of memory。原因YOLOv5 推理时默认 batch size 可能较大或者图片分辨率太高。CCPD 原图有的超过 2000 像素宽直接送进网络会占用大量显存。解决推理时设置model.conf 0.5和model.iou 0.3减少候选框数量同时用size640限制输入分辨率。如果还是溢出把图片先 resize 到 1280 宽再推理。5.5 车牌倾斜导致识别率骤降现象水平车牌识别很准但倾斜角度超过 15 度的车牌识别率掉到 50% 以下。原因CNN 训练时用的裁剪图片都是水平矫正过的模型没见过倾斜样本。解决在数据增强里加随机旋转角度范围设(-10, 10)度。如果倾斜角度更大需要先做透视变换矫正。CCPD 文件名里第 4 段是四个角点坐标可以用cv2.getPerspectiveTransform做矫正把车牌拉成标准矩形再送进 CNN。6. 进阶技巧用 TensorRT 加速推理与树莓派部署验证训练完模型只是第一步真正落地时推理速度往往比精度更关键。我习惯用 TensorRT 对 YOLOv5 做加速在 RTX 3060 上能把单张推理从 25ms 压到 8ms 左右。导出 ONNX 再转 TensorRT 引擎的命令如下python export.py --weights runs/train/ccpd_exp/weights/best.pt --include onnx trtexec --onnxbest.onnx --saveEnginebest.engine --fp16--fp16开启半精度推理精度损失通常在 0.5% 以内速度提升接近一倍。转完之后用pycuda加载引擎做推理注意输入输出的绑定名字要和导出时一致。如果想在树莓派 5 上部署思路要换一下。树莓派的 GPU 不支持 CUDA只能用 CPU 推理或者外接加速棒。CPU 推理 YOLOv5s 单张大约 300~500ms做实时检测不现实但做离线批量处理够用。我一般会把 YOLOv5 换成 YOLOv5nnano 版本参数量从 7.2M 降到 1.9MCPU 推理能到 150ms 左右。CNN 识别模型本身很小树莓派上跑一次只要 10ms 以内不是瓶颈。验证部署效果时不要只看单张图片的推理结果。我习惯用 CCPD 验证集里的 1000 张图片做批量测试统计端到端的车牌全对率。如果全对率低于 85%回头检查检测框的裁剪是否完整以及 CNN 输入图片的预处理是否和训练时一致。预处理不一致是部署阶段最常见的坑训练时用了归一化推理时忘了做结果就是模型输出完全不可用。最后说一个我踩过的坑导出 ONNX 时如果动态轴设置不对TensorRT 会固定 batch size 为 1批量推理时直接报错。导出命令里加--dynamic参数可以避免这个问题。部署这件事模型训练只是上半场推理优化和预处理对齐才是决定能不能用的下半场。希望帮到你。本文还有配套的精品资源点击获取