ARTICLE DETAIL

资讯详情

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

基于CNN与YOLOv5的车牌检测识别:从CCPD数据集到模型部署全流程

基于CNN与YOLOv5的车牌检测识别:从CCPD数据集到模型部署全流程 简介本资源面向计算机视觉方向的毕业设计、课程设计及学科竞赛参与者提供一套基于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-226469_448550-444551_453553_449555_446557-0_0_22_27_27_27_24-66-88.jpg按短横线切分后各字段含义如下字段位置含义示例值第 1 段区域编号025第 2 段水平倾斜角与垂直倾斜角95_113第 3 段车牌边界框左上与右下坐标226469_448550第 4 段四个角点坐标444551_453553_449555_446557第 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 段是边界框格式为 x1y1_x2y2 def parse_bbox_from_name(fname): stem os.path.splitext(fname)[0] parts stem.split(-) # 第 3 段索引为 2形如 226469_448550 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_w720, img_h1160): os.makedirs(out_dir, exist_okTrue) 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 f0 {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 python3.9 -y conda activate plate # 按自己显卡驱动选对应 CUDA 版本的 torch pip install torch1.13.1 torchvision0.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但车牌场景我一般只动三个参数默认值车牌场景建议原因lr00.010.001迁移学习时学习率太大会破坏预训练特征anchor默认 COCO用 kmeans 重聚类车牌宽高比和 COCO 目标差异大mosaic1.00.5马赛克增强对小目标有帮助但过强会引入噪声anchor 重聚类可以用 YOLOv5 自带的脚本python utils/autoanchor.py --data ccpd.yaml --img 640它会根据你的数据集重新算 anchor 尺寸车牌是扁长矩形默认 anchor 里没有匹配的形状不重聚类会明显掉点。mosaic 增强调到 0.5 是因为 CCPD 里已经有大量模糊和倾斜样本再叠加过强的马赛克增强反而让模型学到无关特征。3.4 训练过程怎么判断有没有跑偏训练启动后重点看三个指标box_loss、obj_loss和mAP0.5。正常情况 box_loss 在前 10 个 epoch 快速下降obj_loss 缓慢下降mAP 稳步上升。如果 box_loss 震荡不降大概率是学习率太大或标签有问题如果 mAP 一直卡在很低的值先检查标签路径对不对再检查 anchor 是否匹配。验证集上的mAP0.5到 0.95 以上基本可用低于 0.85 就要回头查数据。4. CNN 字符识别头从车牌裁剪到字符序列输出4.1 为什么识别不用 YOLOv5 而单独上 CNNYOLOv5 能做检测但直接让它输出字符序列并不合适。车牌识别本质是序列识别问题字符之间有固定位置关系用检测框逐个框字符再分类遇到字符粘连或模糊就会漏框。更稳的做法是检测框裁出车牌后用一个 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 到 94x24import 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.backbone 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), ) 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, devicecuda): 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[fchar_{i} for i in range(7)], dynamic_axes{input: {0: batch}}, opset_version11 )导出后可以用 onnxruntime 做推理速度比 PyTorch 原生推理快不少尤其是在没有 GPU 的部署环境里。如果还要进一步压速度可以对识别网络做动态量化from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( plate_rec.onnx, plate_rec_int8.onnx, weight_typeQuantType.QUInt8 )量化后模型体积大概降到原来的四分之一CPU 推理速度提升明显精度损失通常在 1 个百分点以内。检测模型量化要谨慎YOLOv5 的卷积层对量化比较敏感建议只量化识别头。验证导出是否正确用一张测试图跑一遍 ONNX 和 PyTorch 的结果对比输出差异应该在 1e-3 以内。如果差异过大检查 opset 版本和输入尺寸是否和导出时一致。我自己的习惯是每次改完模型结构或预处理先导出 ONNX 跑一张图对齐数值再上批量测试。这个习惯帮我省过好几次「本地好好的、部署就崩」的后悔药。车牌检测识别这条链路不算复杂但检测和识别两个环节的预处理必须严格对齐任何一边的归一化或通道顺序变了端到端结果就会崩。希望帮到你。本文还有配套的精品资源点击获取
返回列表