ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于CCPD数据集与YOLOv5的车牌检测识别全流程实战

基于CCPD数据集与YOLOv5的车牌检测识别全流程实战 简介本资源面向计算机视觉方向的毕业设计、课程设计及学科竞赛参与者提供一套基于CNN与YOLOv5的车牌检测与识别完整工程数据集采用CCPD官方数据可帮助读者快速搭建车牌识别实验环境并完成项目复现。压缩包共10个文件约44.33MB包含Python脚本、模型权重文件、YAML配置、Keras模型文件、Jupyter Notebook及README说明文档覆盖从数据配置、模型训练到车牌检测与识别的完整流程。已有79人学习关注适合具备一定深度学习基础、需要完整项目参考或二次开发的学习者。资源内代码经过测试运行功能完整可直接复现也可在此基础上修改扩展实现其他功能设计报告亦可借鉴。项目涵盖YOLOv5目标检测与CNN字符识别两大模块对理解车牌定位、字符分割与识别等关键环节具有较高参考价值适合用于毕设、课设、大作业及工程实训等场景。1. 车牌检测与识别从 CCPD 数据集到 YOLOv5 的完整落地路径拿到一个车牌检测与识别的需求很多人第一反应是直接上 OCR 大模型但实际工程里更稳的方案是「检测 识别」两段式先用 YOLOv5 把车牌框出来再用轻量 CNN 做字符分类。这套组合在 CCPD 数据集上跑通之后单张 1080P 图片的端到端耗时可以压到 30ms 以内mAP0.5 稳定在 0.95 以上。CCPD 官方数据集提供了超过 25 万张带标注的中国车牌图像覆盖了不同光照、倾斜、雨雪和遮挡场景是目前中文车牌任务里最扎实的公开数据源。这篇文章面向的是需要把车牌检测识别真正跑起来的工程师和学生从数据准备、模型训练、后处理到部署量化每一步都给出可复现的命令和参数同时把我在实际项目里踩过的坑摊开讲清楚。2. CCPD 数据集拆解与 YOLOv5 训练前的数据工程2.1 CCPD 的目录结构与标注格式到底怎么读CCPD 官方数据集下载下来通常是一个压缩包解压后是若干文件夹文件名本身就是标注信息。这是 CCPD 最特殊的地方——它没有单独的 XML 或 JSON 标注文件所有信息编码在文件名里。一个典型的文件名长这样025-95_113-184478_446548-446548_184548_184478_446478-0_0_22_27_27_33_16-66-88.jpg按-分割后各字段含义如下字段位置含义示例值第1段面积比025第2段倾斜程度95_113第3段边界框坐标184478_446548第4段四角点坐标446548_184548_184478_446478第5段车牌号码索引0_0_22_27_27_33_16第6段亮度66第7段模糊度88第3段的184478_446548就是我们要的检测框左上角(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段是边界框: x1y1_x2y2 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 f0 {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(parentsTrue, exist_okTrue) lbl_out.mkdir(parentsTrue, exist_okTrue) jpgs glob.glob(os.path.join(ccpd_dir, **, *.jpg), recursiveTrue) 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 输入下 mAP0.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.005lrf: 0.1warmup_epochs: 3。如果是从yolov5s.pt预训练权重开始前 5 个 epoch 可以冻结 backbone只训练 headpython 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 3030 个 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_classes65): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, 3, padding1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2), # 32x32 - 16x16 nn.Conv2d(32, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2), # 16x16 - 8x8 nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2), # 8x8 - 4x4 nn.Conv2d(128, 128, 3, padding1), 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-3batch 12820 个 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, devicecuda): 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:(i1)*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(dim1).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 推理大概 15fpsINT8 量化后能到 30fps。量化时注意校准集要用真实场景图片不能用 CCPD 的图否则量化误差会很大。5.3 模型导出与 ONNX 推理的注意事项YOLOv5 导出 ONNXpython export.py --weights runs/train/ccpd_plate/weights/best.pt --include onnx --img 640 --batch 1导出后检查输入输出节点名YOLOv5 默认输出是output形状[1, 25200, 6]。后处理需要自己做 NMSOpenCV 的cv2.dnn.NMSBoxes就能用。字符 CNN 导出类似注意输入维度改成[1, 1, 32, 32]。注意ONNX 推理时图片预处理要和训练时完全一致包括归一化方式YOLOv5 是/255.0和通道顺序RGB。很多部署翻车都是因为预处理不一致模型本身没问题。6. 把 mAP 再提两个点难例挖掘与测试时增强的实战技巧训练完第一版模型后如果 mAP0.5 卡在 0.95 上不去可以试试难例挖掘。具体做法是用当前模型对训练集做推理把预测框和真实框 IoU 在 0.3~0.5 之间的样本挑出来这些就是「模型觉得像但没对准」的难例。把这些样本复制一份在训练集里增加它们的权重比如重复 3 次再 fine-tune 20 个 epoch。我在 CCPD 上这么操作mAP0.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 再送进字符 CNNNMS 可以用 ONNX 的NonMaxSuppression节点实现。这套方案我从数据转换到部署跑通大概花了三天其中两天在调数据格式和预处理对齐。如果你也在做车牌相关的东西建议先把 CCPD 的文件名解析和 YOLO 格式转换写扎实后面训练和部署会顺很多。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进