ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

基于YOLOv5与CNN的车牌检测识别实战:CCPD数据集全流程 简介这份资源面向计算机视觉方向的毕业设计、课程设计、大作业与竞赛实训场景提供一套基于CNN与YOLOv5的车牌检测与识别完整工程数据集采用CCPD官方数据集适合具备一定深度学习基础、希望复现或二次开发车牌识别项目的学生与开发者。压缩包共10个文件约44.33MB包含Python脚本、YOLOv5权重文件、TensorFlow SavedModel与Keras元数据、YAML配置、Jupyter Notebook及README说明文档覆盖模型训练、检测推理与字符识别等环节结构清晰便于按模块查阅。目前已有79人学习关注。资源内代码经过测试运行功能完整可复现出与作者一致的项目效果设计报告亦可借鉴读者可据此掌握YOLOv5车牌定位与CNN字符识别的联合流程理解数据集组织、模型加载与推理脚本的衔接方式并在此基础上扩展出更多功能适合作为项目立项、学习练手与答辩展示的参考。1. 车牌检测识别这套 CNN YOLOv5 方案到底值不值得做停车场出入口的相机拍到一张图你得在几百毫秒内告诉闸机「京A·12345放行」。这件事拆开就是两步先在整张图里把车牌那个小矩形框出来再把框里的字符逐个认出来。前者是目标检测后者是字符识别。标题里的方案用 YOLOv5 做检测、用 CNN 做识别数据集用 CCPD。这套组合在毕设、课设、实训里出现频率极高原因很实在YOLOv5 工程化程度高、训练脚本开箱即用CCPD 又是国内少有的、规模够大且带完整标注的中文车牌数据集两者拼起来能在单卡上跑通全流程。它适合谁如果你要交一个能演示、能讲清原理、还能自己换数据重训的完整项目这套方案是稳妥选择。如果你指望它直接上生产、应对暴雨逆光和严重倾斜那得先看清它的边界。下面按「数据怎么准备 → 检测怎么训 → 识别怎么做 → 坑在哪 → 怎么验证」的顺序讲透参数和命令都给到能直接抄的程度。2. CCPD 数据集从原始标注到 YOLOv5 可训练格式2.1 CCPD 的目录结构和标注含义CCPD 全称 Chinese City Parking Dataset采集自合肥市路边停车场景图片以车牌区域为核心分辨率多为 720×1160。它的标注不走 XML 或 JSON而是直接编码在文件名里这是很多人第一次用会懵的地方。一个典型文件名长这样025-95_113-226469_448522-444521_232527_235474_446468-0_0_22_27_27_33_16-66-88.jpg按-切分后各字段含义如下字段位置含义示例值0区域编号0251倾斜角度水平、垂直95_1132边界框左下、右上坐标226469_4485223车牌四角坐标444521_232527_235474_4464684四个角标亮度0_0_22_275车牌号码索引27_27_33_166亮度、模糊度66_88字段 2 的226469_448522就是我们要的检测框x_min226, y_min469, x_max448, y_max522。字段 5 是车牌字符在省份、字母、数字字典里的索引识别阶段要用。提示CCPD 官方分了好几个子集CCPD-Base 是标准场景CCPD-Green 是新能源绿牌CCPD-Blur 是模糊图CCPD-Tilt 是倾斜图。做毕设建议先用 Base 跑通再拿 Green 和 Tilt 做鲁棒性对比这样报告里有东西可写。2.2 把文件名标注转成 YOLO 的 txt 标签YOLOv5 要的是每张图对应一个 txt每行class cx cy w h坐标全部归一化到 0~1。CCPD 只有「车牌」一个类别class 恒为 0。转换脚本如下import os import glob # CCPD 图片目录和输出标签目录 img_dir ./CCPD2019/ccpd_base label_dir ./labels os.makedirs(label_dir, exist_okTrue) for img_path in glob.glob(os.path.join(img_dir, *.jpg)): name os.path.basename(img_path).split(-)[2] # 取边界框字段 # 字段形如 226469_448522 x_min, y_min map(int, name.split(_)[0].split()) x_max, y_max map(int, name.split(_)[1].split()) # 读取图片真实尺寸做归一化不要写死 720x1160 from PIL import Image w_img, h_img Image.open(img_path).size cx (x_min x_max) / 2.0 / w_img cy (y_min y_max) / 2.0 / h_img w (x_max - x_min) / w_img h (y_max - y_min) / h_img txt_name os.path.splitext(os.path.basename(img_path))[0] .txt with open(os.path.join(label_dir, txt_name), w) as f: f.write(f0 {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n)逻辑说明先按-切分文件名拿到第 3 段索引 2再按_拆成左下和右上两组坐标各自按拆出 x、y。归一化时务必用Image.open().size读真实宽高CCPD 里存在少量尺寸不一致的图写死 720×1160 会让框整体偏移。参数上:.6f保留六位小数足够YOLOv5 内部还会再处理。2.3 划分训练集验证集并生成 data.yaml转换完标签后按 8:1:1 划分 train/val/test然后写 YOLOv5 需要的配置文件# data.yaml path: ./ccpd_dataset train: images/train val: images/val test: images/test nc: 1 names: [plate]nc是类别数车牌只有一类所以是 1。names顺序必须和转换脚本里写的 class 编号一致否则训练时标签全错位。目录结构保持images/train和labels/train平行YOLOv5 会自动把images替换成labels去找标签文件。3. YOLOv5 训练车牌检测参数怎么设、什么时候停3.1 选哪个预训练权重和模型尺寸YOLOv5 有 n/s/m/l/x 五个尺寸参数量从 1.9M 到 86M 递增。车牌检测属于单类、目标形状规整的任务不需要超大模型。我一般从yolov5s.pt起步mAP 不够再换yolov5m.pt。n 版本虽然快但小目标召回率明显偏低CCPD 里远距离车牌容易漏检。# 克隆官方仓库并安装依赖 git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt # 开始训练 python train.py \ --img 640 \ --batch 16 \ --epochs 100 \ --data ./data.yaml \ --weights yolov5s.pt \ --cfg models/yolov5s.yaml \ --device 0 \ --workers 8 \ --project runs/train \ --name ccpd_s参数逐个说--img 640是输入分辨率车牌在 720×1160 原图里占比不大640 够用显存紧张可以降到 512。--batch 16在 8G 显存上跑 640 分辨率比较稳爆显存就减半。--epochs 100是上限实际靠早停。--device 0指定第一块 GPUCPU 训练把这里改成cpu但会很慢。--workers 8是数据加载线程数Windows 上设太大会报错建议 4 以下。3.2 超参数里真正影响车牌检测的几个YOLOv5 的超参数在data/hyp.scratch.yaml里大部分默认值对车牌任务够用但有几个值得动参数默认值车牌任务建议原因lr00.010.01从预训练权重微调保持默认lrf0.010.01余弦退火终值不用改momentum0.9370.937SGD 动量稳定weight_decay0.00050.0005正则化防过拟合warmup_epochs3.03.0前 3 轮预热避免初期震荡box0.050.05框回归损失权重cls0.50.5分类损失权重单类任务够用anchor_t4.04.0锚框匹配阈值真正要调的是数据增强部分。CCPD 里车牌有各种倾斜degrees可以设 10 左右增加旋转样本mosaic默认 1.0 保留能提升小目标检测但mixup对车牌这种结构化目标帮助不大可以设 0。3.3 训练过程看什么指标、什么时候该停训练日志里重点盯三个box_loss、obj_loss、mAP0.5。正常情况下 box_loss 从 0.08 左右稳步降到 0.02 以下obj_loss 从 0.05 降到 0.01 附近。如果 box_loss 震荡不降多半是学习率太大或标签有问题。mAP0.5 在 30 轮左右应该能到 0.95 以上CCPD-Base 场景干净这个数字不难达到。早停靠--patience参数默认 30 轮没提升就停。我一般设 20省时间。训练完在runs/train/ccpd_s/weights/下会有best.pt和last.pt部署用 best。# 用验证集跑一次评估看各类指标 python val.py \ --weights runs/train/ccpd_s/weights/best.pt \ --data ./data.yaml \ --img 640 \ --task val输出里mAP0.5和mAP0.5:0.95都要看。前者宽松后者严格。车牌检测框比较规整两者差距通常在 0.1 以内如果差距过大说明框回归不够准可以适当提高box损失权重。4. CNN 字符识别从裁剪车牌到输出号码4.1 识别网络的输入怎么来检测阶段输出的是车牌框坐标识别阶段要把框里的区域裁出来再送进 CNN。裁剪时按框坐标外扩 5~10 像素避免字符被切边。裁完统一 resize 到固定尺寸常见做法是 94×24 或 128×32宽高比接近真实车牌。CCPD 的字符集是固定的31 个省份简称 24 个字母去掉 I 和 O 10 个数字共 65 类。车牌号码通常是 7 位新能源 8 位所以识别网络输出是 7 个位置、每个位置 65 类的分类。4.2 一个能跑通的 CNN 识别网络结构不用上 ResNet车牌字符识别用 5 层卷积就够。下面是一个可直接用的 PyTorch 定义import torch import torch.nn as nn class PlateCNN(nn.Module): def __init__(self, num_chars65, seq_len7): super().__init__() self.seq_len seq_len self.num_chars num_chars # 特征提取5 层卷积逐步下采样 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.MaxPool2d(2), nn.Conv2d(256, 256, 3, padding1), nn.ReLU(), ) # 输入 128x32 经过 4 次池化变成 8x2展平后 256*8*24096 self.fc nn.Sequential( nn.Linear(256 * 8 * 2, 512), nn.ReLU(), nn.Dropout(0.3), ) # 7 个位置各自输出 65 类 self.head nn.Linear(512, seq_len * num_chars) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) x self.fc(x) x self.head(x) # 重塑成 [batch, seq_len, num_chars] return x.view(-1, self.seq_len, self.num_chars)逻辑说明卷积部分负责提取字符的笔画和结构特征四次池化把 128×32 压到 8×2。全连接层把特征映射到 512 维再通过 head 输出 7×65 的 logits。损失函数用CrossEntropyLoss把 7 个位置当作 7 个独立分类任务每个位置算一次交叉熵再求平均。参数上num_chars65要和你的字典严格对齐字典顺序错了识别结果全乱。seq_len7是普通车牌位数新能源要改成 8。Dropout(0.3)防过拟合数据量够大可以降到 0.2。4.3 训练识别网络的损失和优化器设置model PlateCNN().cuda() criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.5) for epoch in range(60): model.train() for imgs, labels in train_loader: # labels 形状 [batch, 7] imgs, labels imgs.cuda(), labels.cuda() preds model(imgs) # [batch, 7, 65] # 逐位置计算损失 loss sum(criterion(preds[:, i, :], labels[:, i]) for i in range(7)) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()逻辑说明Adam 学习率 1e-3 是识别任务的常用起点配合 StepLR 每 20 轮衰减一半。损失按位置累加这样每个字符位置都能得到监督。如果某一位比如省份简称识别特别差可以给该位置加权但一般不需要。评估时用逐位准确率和整牌准确率两个指标。逐位准确率看每个字符对不对整牌准确率要求 7 位全对。CCPD-Base 上整牌准确率做到 95% 以上算正常低于 90% 要检查字典映射和裁剪质量。5. 避坑与排查这套方案最容易翻车的五个地方5.1 检测框坐标偏移裁出来的车牌缺字现象识别准确率上不去可视化发现裁剪图边缘字符被切掉。 原因CCPD 文件名里的边界框是紧贴字符的直接按框裁剪会切边。 解决裁剪时外扩 8~10 像素同时限制不超出图片边界。代码里用max(0, x_min-8)和min(w, x_max8)做钳制。5.2 训练 loss 正常但 mAP 极低现象box_loss 一直降但 val 的 mAP 始终在 0.1 以下。 原因data.yaml 里的names顺序和标签文件里的 class 编号不一致或者标签归一化时用了错误的图片尺寸。 解决随机抽 5 张图用 YOLOv5 自带的utils/plots.py把标签画到图上肉眼核对框位置对不对一眼就能看出来。5.3 识别网络把「0」和「O」、「1」和「I」混淆现象整牌准确率卡在 85% 左右错误集中在几个形近字符。 原因CCPD 字典本身去掉了 I 和 O但如果你自己扩了字典或用了别的数据集就会引入混淆。 解决严格使用 CCPD 的 65 类字典不要自行增删。如果必须加给形近字符加区分性样本。5.4 显存爆了batch 调小后 mAP 下降现象把 batch 从 16 降到 8 后同样的 epoch 数 mAP 低了 2 个点。 原因batch 变小导致梯度噪声增大等效学习率变了。 解决batch 减半时学习率也按比例降或者用梯度累积模拟大 batch。YOLOv5 支持--accumulate参数。5.5 推理速度慢单张图超过 200ms现象部署到边缘设备后帧率不达标。 原因输入分辨率设太高或者没做半精度推理。 解决推理时用--half开启 FP16分辨率从 640 降到 416 通常能提速一倍mAP 只掉 1 个点左右。识别网络可以转 ONNX 再用 ONNXRuntime 跑比原生 PyTorch 快不少。6. 端到端串联与效果验证一个可复现的推理脚本把检测和识别串起来核心是「检测输出框 → 裁剪 → 识别 → 拼字符串」。下面这个脚本可以直接跑import cv2 import torch from PIL import Image # 加载检测模型和识别模型 det_model torch.hub.load(ultralytics/yolov5, custom, pathruns/train/ccpd_s/weights/best.pt) rec_model PlateCNN().cuda().eval() rec_model.load_state_dict(torch.load(plate_cnn.pth)) # 65 类字典顺序必须和训练时一致 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 recognize(img_path): img cv2.imread(img_path) results det_model(img) boxes results.xyxy[0].cpu().numpy() plates [] for box in boxes: x1, y1, x2, y2 map(int, box[:4]) # 外扩 8 像素并钳制边界 x1, y1 max(0, x1-8), max(0, y1-8) x2, y2 min(img.shape[1], x28), min(img.shape[0], y28) crop img[y1:y2, x1:x2] crop cv2.resize(crop, (128, 32)) crop cv2.cvtColor(crop, cv2.COLOR_BGR2RGB) tensor torch.from_numpy(crop).permute(2, 0, 1).float() / 255.0 tensor tensor.unsqueeze(0).cuda() with torch.no_grad(): preds rec_model(tensor) # [1, 7, 65] idx preds.argmax(dim2)[0].cpu().numpy() plate_str .join(CHARS[i] for i in idx) plates.append((plate_str, (x1, y1, x2, y2))) return plates逻辑说明检测模型用torch.hub.load加载本地权重返回的xyxy是绝对坐标。裁剪后 resize 到 128×32转 RGB 再归一化到 0~1。识别模型输出 argmax 拿到每个位置的索引查字典拼成字符串。注意CHARS列表里省份简称有重复皖出现两次实际使用时按你训练时的字典顺序来这里只是示意。验证方法上我习惯分三层第一层用 CCPD 的 test 集跑整牌准确率第二层自己拍 20 张不同光照的图看泛化第三层录一段视频看连续帧的稳定性。三层都过了这个方案才算真正跑通。最后说个血泪经验字典顺序和标签映射这两件事我前后翻车过三次每次都是训练指标好看但推理结果乱码。后来养成的习惯是训练前先把字典存成 json推理时直接读同一个文件从根上杜绝不一致。希望帮到你。本文还有配套的精品资源点击获取
返回列表