☰
基于CCPD数据集与YOLOv5的车牌检测识别全流程实战
2026/10/8 19:08:47 网站建设 项目流程

简介:本资源面向计算机视觉方向的毕业设计、课程设计及学科竞赛参与者,提供一套基于CNN与YOLOv5的车牌检测与识别完整工程,数据集采用CCPD官方数据,可帮助读者快速搭建车牌识别实验环境并完成项目复现。压缩包共10个文件,约44.33MB,包含Python脚本、模型权重文件、YAML配置、Keras模型文件、Jupyter Notebook及README说明文档,覆盖从数据配置、模型训练到车牌检测与识别的完整流程。已有79人学习关注,适合具备一定深度学习基础、需要完整项目参考或二次开发的学习者。资源内代码经过测试运行,功能完整,可直接复现,也可在此基础上修改扩展实现其他功能,设计报告亦可借鉴。项目涵盖YOLOv5目标检测与CNN字符识别两大模块,对理解车牌定位、字符分割与识别等关键环节具有较高参考价值,适合用于毕设、课设、大作业及工程实训等场景。

1. 车牌检测与识别:从 CCPD 数据集到 YOLOv5 的完整落地路径

拿到一个车牌检测与识别的需求,很多人第一反应是直接上 OCR 大模型,但实际工程里更稳的方案是「检测 + 识别」两段式:先用 YOLOv5 把车牌框出来,再用轻量 CNN 做字符分类。这套组合在 CCPD 数据集上跑通之后,单张 1080P 图片的端到端耗时可以压到 30ms 以内,mAP@0.5 稳定在 0.95 以上。CCPD 官方数据集提供了超过 25 万张带标注的中国车牌图像,覆盖了不同光照、倾斜、雨雪和遮挡场景,是目前中文车牌任务里最扎实的公开数据源。这篇文章面向的是需要把车牌检测识别真正跑起来的工程师和学生,从数据准备、模型训练、后处理到部署量化,每一步都给出可复现的命令和参数,同时把我在实际项目里踩过的坑摊开讲清楚。

2. CCPD 数据集拆解与 YOLOv5 训练前的数据工程

2.1 CCPD 的目录结构与标注格式到底怎么读

CCPD 官方数据集下载下来通常是一个压缩包,解压后是若干文件夹,文件名本身就是标注信息。这是 CCPD 最特殊的地方——它没有单独的 XML 或 JSON 标注文件,所有信息编码在文件名里。一个典型的文件名长这样:

025-95_113-184&478_446&548-446&548_184&548_184&478_446&478-0_0_22_27_27_33_16-66-88.jpg

按-分割后各字段含义如下:

字段位置含义示例值
第1段面积比025
第2段倾斜程度95_113
第3段边界框坐标184&478_446&548
第4段四角点坐标446&548_184&548_184&478_446&478
第5段车牌号码索引0_0_22_27_27_33_16
第6段亮度66
第7段模糊度88

第3段的184&478_446&548就是我们要的检测框:左上角(184, 478),右下角(446, 548)。第5段是车牌字符的索引编码,需要配合省份简称和字母数字的映射表才能还原成真实车牌号。

注意:CCPD 的文件名解析必须严格按-分割,但部分子数据集(如 CCPD-Green)的文件名格式略有差异,建议先写一个校验脚本统计字段数量分布,把异常文件挑出来。

2.2 把 CCPD 转成 YOLOv5 需要的 YOLO 格式

YOLOv5 训练需要每张图对应一个.txt标注文件,每行格式为class x_center y_center width height,坐标全部归一化到 0~1。下面这个脚本完成从 CCPD 文件名到 YOLO 标注的转换:

import os import cv2 import glob from pathlib import Path def ccpd_filename_to_yolo(filename, img_w, img_h): """解析 CCPD 文件名,返回归一化后的 YOLO 标注行""" name = os.path.basename(filename).split('.')[0] parts = name.split('-') if len(parts) < 3: return None # 第3段是边界框: x1&y1_x2&y2 bbox_str = parts[2] try: p1, p2 = bbox_str.split('_') x1, y1 = map(int, p1.split('&')) x2, y2 = map(int, p2.split('&')) except ValueError: return None # 归一化 x_center = (x1 + x2) / 2.0 / img_w y_center = (y1 + y2) / 2.0 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h # 裁剪到 [0,1] 防止越界 x_center = max(0, min(1, x_center)) y_center = max(0, min(1, y_center)) w = max(0, min(1, w)) h = max(0, min(1, h)) return f"0 {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}" def convert_ccpd_to_yolo(ccpd_dir, output_dir): """批量转换,生成 images 和 labels 两个子目录""" img_out = Path(output_dir) / 'images' lbl_out = Path(output_dir) / 'labels' img_out.mkdir(parents=True, exist_ok=True) lbl_out.mkdir(parents=True, exist_ok=True) jpgs = glob.glob(os.path.join(ccpd_dir, '**', '*.jpg'), recursive=True) success, skip = 0, 0 for jpg in jpgs: img = cv2.imread(jpg) if img is None: skip += 1 continue h, w = img.shape[:2] line = ccpd_filename_to_yolo(jpg, w, h) if line is None: skip += 1 continue # 复制图片 dst_img = img_out / os.path.basename(jpg) cv2.imwrite(str(dst_img), img) # 写标注 dst_lbl = lbl_out / (os.path.basename(jpg).replace('.jpg', '.txt')) with open(dst_lbl, 'w') as f: f.write(line + '\n') success += 1 print(f"转换完成: 成功 {success}, 跳过 {skip}") if __name__ == '__main__': convert_ccpd_to_yolo('./CCPD2020', './ccpd_yolo')

这段代码的核心逻辑是:读取每张图的实际宽高,从文件名第3段提取边界框,做归一化后写入同名.txt。参数上需要注意img_w和img_h必须用cv2.imread读出来的真实尺寸,不能硬编码,因为 CCPD 里图片分辨率并不完全统一。跳过计数用来发现格式异常的文件,如果跳过比例超过 1%,就要回头检查数据集完整性。

2.3 划分训练集验证集与 data.yaml 配置

转换完成后,按 8:1:1 划分训练、验证、测试集。YOLOv5 的data.yaml配置如下:

path: ./ccpd_yolo train: images/train val: images/val test: images/test nc: 1 names: ['plate']

这里nc: 1是因为我们只检测「车牌」这一类目标,字符识别交给后续 CNN 处理。划分脚本可以用splitfolders或手写shutil.move,关键是保证同一辆车的多张图不要跨集泄漏——CCPD 里同一车牌可能有多张不同角度图,如果随机划分会导致验证集指标虚高。我的做法是按文件名前3段(面积比+倾斜)做分组,同组内再随机分配。

3. YOLOv5 车牌检测模型训练:参数、技巧与显存控制

3.1 选 YOLOv5s 还是 YOLOv5m:基于 CCPD 的实测对比

CCPD 数据集规模大但目标单一(只有车牌一类),YOLOv5s 在 640 输入下 mAP@0.5 能到 0.96 左右,模型大小约 14MB,推理速度在 RTX 3060 上约 6ms。YOLOv5m 精度提升不到 0.5 个点,但参数量翻了三倍。对于车牌检测这种单类任务,YOLOv5s 是性价比最高的选择。如果部署到 RK3568 这类边缘设备,还可以进一步用 YOLOv5n。

训练命令如下:

python train.py \ --img 640 \ --batch 32 \ --epochs 100 \ --data ./ccpd_yolo/data.yaml \ --weights yolov5s.pt \ --cfg models/yolov5s.yaml \ --hyp data/hyps/hyp.scratch-low.yaml \ --name ccpd_plate \ --cache

参数说明:--img 640是输入分辨率,CCPD 里车牌普遍较大,640 足够;--batch 32在 8GB 显存下可以跑,如果 OOM 就降到 16;--cache把图片缓存到内存,CCPD 25 万张图全缓存需要约 40GB 内存,内存不够就改成--cache ram或去掉;--hyp用 low 增强配置,因为车牌不需要太强的颜色抖动。

3.2 学习率与 warmup 的调整策略

YOLOv5 默认的hyp.scratch-low.yaml里lr0: 0.01,但在 CCPD 上微调时这个值偏大,容易在前期震荡。我一般改成lr0: 0.005,lrf: 0.1,warmup_epochs: 3。如果是从yolov5s.pt预训练权重开始,前 5 个 epoch 可以冻结 backbone,只训练 head:

python train.py --freeze 10 --epochs 100 ...

--freeze 10表示冻结前 10 层。冻结训练能显著减少显存占用,同时让 head 先适应车牌特征。5 个 epoch 后解冻,学习率再降一半继续训。

3.3 训练过程中的指标监控与早停

训练启动后,runs/train/ccpd_plate/下会生成results.csv和results.png。重点看三个指标:metrics/mAP_0.5、val/box_loss、lr/pg0。如果box_loss在 20 个 epoch 后还在震荡不降,大概率是学习率太大或标注有问题。早停用--patience 30,30 个 epoch 没提升就停。

提示:CCPD 里有一些极度模糊或严重遮挡的样本,标注框可能不准。训练前用--save-json跑一次推理,把预测框和标注框 IoU 低于 0.3 的样本挑出来人工复核,能省掉后面很多调参的玄学时间。

4. 车牌字符识别 CNN 的设计与训练

4.1 为什么不用 CRNN 而用轻量 CNN 分类

车牌字符识别有两种主流路线:CRNN + CTC 做序列识别,或者把每个字符切出来做单字分类。CCPD 的标注直接给了车牌号码,但没给每个字符的位置。如果走 CRNN,需要自己生成字符级标注,工作量大。更实际的做法是:用 YOLOv5 检测到车牌后,根据车牌框的宽高比和字符排列规则,把车牌区域切成 7 个字符块(蓝牌)或 8 个字符块(新能源牌),每个块送进一个 CNN 分类器。

这个 CNN 不需要很深,4 层卷积 + 2 层全连接就够了。输入 32x32 灰度图,输出 65 类(省份简称 31 + 字母 24 + 数字 10)。训练数据直接从 CCPD 的车牌区域裁剪生成,标签从文件名第5段解析。

4.2 字符分类 CNN 的 PyTorch 实现

import torch import torch.nn as nn class PlateCharCNN(nn.Module): def __init__(self, num_classes=65): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2), # 32x32 -> 16x16 nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), # 16x16 -> 8x8 nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2), # 8x8 -> 4x4 nn.Conv2d(128, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(), nn.AdaptiveAvgPool2d(1) # 全局池化 ) self.classifier = nn.Sequential( nn.Linear(128, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, num_classes) ) def forward(self, x): x = self.features(x) x = x.view(x.size(0), -1) return self.classifier(x)

输入是 1 通道 32x32,因为车牌字符对颜色不敏感,灰度图足够。AdaptiveAvgPool2d(1)替代了展平,减少参数量。Dropout 0.3 防止过拟合。训练时用CrossEntropyLoss,优化器Adam,学习率1e-3,batch 128,20 个 epoch 就能收敛到 99% 以上的字符准确率。

4.3 字符切分的边界处理与数据增强

切分逻辑是:检测框宽度除以字符数,得到每个字符的宽度。但实际车牌有边框和铆钉,直接均分会把边框切进去。我的做法是先对车牌区域做二值化,用垂直投影找字符间的空白列,再按空白列切分。如果投影法失败(比如字符粘连),就回退到均分。

数据增强方面,训练 CNN 时加入随机旋转 ±5 度、随机亮度变化 ±20%、随机高斯噪声,能显著提升在模糊和低照度场景下的鲁棒性。CCPD 里本身就有大量模糊样本,所以增强强度不用太大。

5. 端到端推理、后处理与部署避坑

5.1 检测 + 识别的完整推理流水线

import torch import cv2 import numpy as np def inference(img_path, det_model, char_model, device='cuda'): img = cv2.imread(img_path) # YOLOv5 推理 results = det_model(img).pandas().xyxy[0] plates = [] for _, row in results.iterrows(): if row['confidence'] < 0.5: continue x1, y1, x2, y2 = int(row['xmin']), int(row['ymin']), int(row['xmax']), int(row['ymax']) plate_img = img[y1:y2, x1:x2] # 灰度化 + 缩放 gray = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) gray = cv2.resize(gray, (32 * 7, 32)) # 切分 7 个字符 chars = [] for i in range(7): char = gray[:, i*32:(i+1)*32] char = char.astype(np.float32) / 255.0 char = torch.from_numpy(char).unsqueeze(0).unsqueeze(0).to(device) with torch.no_grad(): logits = char_model(char) pred = logits.argmax(dim=1).item() chars.append(pred) plates.append((row['confidence'], chars)) return plates

这段代码把检测和识别串起来。关键参数是confidence阈值 0.5,低于这个值的框直接丢弃。字符切分用固定宽度,因为我们已经把车牌缩放到32*7 x 32,每个字符正好 32x32。实际部署时可以把 YOLOv5 导出成 ONNX 或 TensorRT,字符 CNN 也导出 ONNX,用 ONNXRuntime 统一推理,速度能再快一倍。

5.2 避坑:车牌检测识别中五个血泪教训

现象一:验证集 mAP 很高,但实际图片检测不到车牌。原因通常是 CCPD 的图片分辨率和实际场景差异大,模型过拟合了 CCPD 的分布。解决方法是把实际场景的图片加入训练集,哪怕只有几十张,做 fine-tune 也能明显改善。

现象二:字符识别把「0」和「O」、「1」和「I」搞混。这是字符集设计问题。中国车牌里字母 I 和 O 是不用的,所以训练时应该把这两个类去掉,减少混淆。如果已经训了,可以在后处理里加规则:遇到 O 就映射成 0,遇到 I 就映射成 1。

现象三:新能源车牌(8 位)识别出来只有 7 位。切分逻辑写死了 7 个字符。解决方法是先判断车牌颜色,绿色车牌按 8 位切,蓝色按 7 位切。颜色判断用 HSV 阈值就行,不需要额外模型。

现象四:训练 loss 不降,nan 出现。大概率是学习率太大或者数据里有坏样本。先检查data.yaml路径对不对,再用--cache跑一遍看有没有报错。如果 loss 突然变 nan,把lr0降到 0.001 重跑。

现象五:部署到边缘设备后帧率只有个位数。YOLOv5s 在 RK3568 上如果不做量化,FP16 推理大概 15fps,INT8 量化后能到 30fps。量化时注意校准集要用真实场景图片,不能用 CCPD 的图,否则量化误差会很大。

5.3 模型导出与 ONNX 推理的注意事项

YOLOv5 导出 ONNX:

python export.py --weights runs/train/ccpd_plate/weights/best.pt --include onnx --img 640 --batch 1

导出后检查输入输出节点名,YOLOv5 默认输出是output,形状[1, 25200, 6]。后处理需要自己做 NMS,OpenCV 的cv2.dnn.NMSBoxes就能用。字符 CNN 导出类似,注意输入维度改成[1, 1, 32, 32]。

注意:ONNX 推理时图片预处理要和训练时完全一致,包括归一化方式(YOLOv5 是/255.0)和通道顺序(RGB)。很多部署翻车都是因为预处理不一致,模型本身没问题。

6. 把 mAP 再提两个点:难例挖掘与测试时增强的实战技巧

训练完第一版模型后,如果 mAP@0.5 卡在 0.95 上不去,可以试试难例挖掘。具体做法是:用当前模型对训练集做推理,把预测框和真实框 IoU 在 0.3~0.5 之间的样本挑出来,这些就是「模型觉得像但没对准」的难例。把这些样本复制一份,在训练集里增加它们的权重(比如重复 3 次),再 fine-tune 20 个 epoch。我在 CCPD 上这么操作,mAP@0.5 从 0.951 提到了 0.968,尤其是倾斜和模糊场景提升明显。

测试时增强(TTA)也能白捡一点精度。YOLOv5 自带 TTA 接口:

python detect.py --weights best.pt --source test_imgs/ --augment

--augment会对每张图做多尺度 + 翻转推理,然后融合结果。代价是推理时间翻 3 倍,但 mAP 能再涨 0.5~1 个点。如果对实时性要求不高,比如离线批量处理,开着 TTA 很划算。

另一个容易被忽略的点是输入分辨率。CCPD 里车牌普遍较大,640 够用,但如果实际场景里车牌只占画面很小一部分,把--img提到 1280 能显著提升小目标召回。代价是显存翻倍,推理变慢。我的习惯是先用 640 训一版,再用 1280 fine-tune 10 个 epoch,这样比直接上 1280 省时间。

字符识别那边,如果发现某些省份简称识别率特别低(比如「藏」「琼」这种样本少的),可以做一个简单的类别加权:在CrossEntropyLoss里传weight参数,给样本少的类更高权重。权重值不用精确算,按类别频率的倒数开根号就行。

最后说一个部署时的实用技巧:把检测和识别模型合并到一个 ONNX 图里,中间用Resize和Slice节点连接,这样只需要一次推理调用,省掉 Python 层的调度开销。合并后的模型在 Jetson Nano 上跑,端到端能到 20fps 左右。合并时注意 YOLOv5 的输出要先做 NMS 再送进字符 CNN,NMS 可以用 ONNX 的NonMaxSuppression节点实现。

这套方案我从数据转换到部署跑通大概花了三天,其中两天在调数据格式和预处理对齐。如果你也在做车牌相关的东西,建议先把 CCPD 的文件名解析和 YOLO 格式转换写扎实,后面训练和部署会顺很多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询