拓冰建站拓冰建站
首页 / 资讯中心 / 正文

CCPD数据集解析与YOLOv5车牌识别全流程实战

简介基于CNN与YOLOv5的车牌检测与识别项目采用CCPD官方数据集是一套面向毕业设计、课程设计、实训与大作业等场景的完整可运行工程。压缩包共含10个文件大小约44.33MB核心内容包括Python源码、YOLOv5权重文件、TensorFlow模型文件、YAML配置、Jupyter Notebook训练脚本及说明文档覆盖模型训练、推理、参数配置等环节能够直接满足复现需求。依托CCPD官方数据集的真实停车场景模型训练数据具有较强代表性项目代码经过严格测试功能正常设计报告可参考答辩平均分达96分完成度较高。目前已有77人学习/下载适合希望在车牌识别方向快速搭建项目、理解CNN与YOLOv5应用流程的开发者。使用中遇到问题可联系作者获得指导便于顺利复现或扩展功能。1. CCPD与YOLOv5车牌识别流水线从文件名里挖出标签CCPD这套车牌数据集的标签不在JSON里、不在xml里而是直接压缩在图片文件名中。第一次用的时候我把它当普通编号跳过结果YOLOv5训练出来框是歪的。直到按官方说明拆开文件名才发现每一段都对应着车牌面积占比、倾斜角、角点坐标和具体车牌号。这里把YOLOv5车牌检测和CNN识别串成一条完整流水线先用YOLOv5从整图中定位车牌区域再把裁剪出的车牌图交给CNN完成字符识别最终输出类似“京A12345”的结果。对做毕业设计、课程设计或工程实训的人来说理解“文件名标签→YOLOv5检测→CNN识别”这条数据链路比跑通单个demo重要得多它能直接决定你复现时数据对不对、训练有没有效、答辩能不能讲清楚。2. 解析CCPD数据集把文件名当标签用转出YOLO训练格式2.1 CCPD的目录结构与文件名分段CCPD全称China City Parking Dataset图片大多拍摄自城市停车场景背景复杂、车辆密集、车牌倾斜角度大。数据集按场景分成了多个子集最常用来做训练的是ccpd_base这个子集里的图片保留了完整的停车场景环境车牌只占画面的一小部分所以YOLOv5能学到“车牌和车脸背景”的区分而不是对着纯车牌图做检测。CCPD最反直觉的设计是它把标注信息全部塞进了文件名。拿一条典型记录来看025-95_113-154383_386473-386473_177454_154383_363402-0_0_22_27_27_33_16-37-15.jpg用-分割后每一段都有明确含义。分段示例含义第1段025车牌面积占整图面积的比例第2段95_113水平倾斜角和垂直倾斜角第3段154383_386473车牌外接矩形的左上角与右下角坐标第4段386473_177454_154383_363402车牌四个顶点的像素坐标顺时针排列第5段0_0_22_27_27_33_16车牌号码每段是字符在映射表中的索引第6段37图像亮度第7段15图像模糊度数值越大越模糊第5段就是训练时唯一的“真实标签”。它一共7位对应大陆车牌的第1位汉字省份简称加后6位字母数字。映射规则常见做法是0-9表示数字10-33附近一段表示字母部分字母如I、O会被跳过再往后是31个省份汉字简称。具体索引顺序在不同版本里略有差异资源里的plate_dec.py维护着一张官方顺序的映射表解密时直接复用它不要自己重新发明。2.2 用Python解析文件名并还原车牌号解析脚本的核心是字符串切分和角点坐标换算。下面这个函数可以直接放在数据处理脚本的开头import re def parse_ccpd_filename(filename): # 去掉扩展名按 - 分段 name filename.replace(.jpg, ) parts name.split(-) # 第3段左上角与右下角坐标格式 154383_386473 lt_x, lt_y, rb_x, rb_y map(int, parts[2].replace(, _).split(_)) # 第4段四个角点顺时针方向用来验证外接矩形 corners list(map(int, parts[3].replace(, _).split(_))) # 第5段车牌字符索引7位数字 plate_idx parts[4].split(_) return { bbox: (lt_x, lt_y, rb_x, rb_y), corners: corners, plate_idx: plate_idx, brightness: int(parts[5]), blur: int(parts[6]) } # 用上面那条文件名测试 info parse_ccpd_filename(025-95_113-154383_386473-386473_177454_154383_363402-0_0_22_27_27_33_16-37-15.jpg) print(info[bbox])这段代码做的事很简单先把文件名按照-切开然后对第3段做两次替换把和_统一成下划线再按_拆出4个整数得到外接矩形的左上和右下坐标。这里有一个容易踩的细节CCPD的第4段角点坐标同样用连接如果直接按split(_)处理拿到的不是干净的数字必须先replace成统一分隔符再转int否则类型转换会报错。拿到bbox后车牌区域就有了。这一步是整个数据准备的地基后面的YOLO格式标签、训练时的样本裁剪全部依赖这一段解析逻辑。至于第5段的plate_idx先原样保存等到写评估脚本时再查表还原成字符串。2.3 转成YOLO格式并切分训练集与验证集YOLOv5不认CCPD的角点格式它需要的是归一化后的x_center y_center width height。转换时直接用外接矩形四个值去算中心点和宽高再除以图片尺寸。因为CCPD的标注本身是近似轴对齐的矩形直接取外接矩形不会引入额外噪声不需要做旋转框处理。import os import random import cv2 def ccpd_to_yolo(img_path, tag, save_dir): # 读取图片尺寸用来归一化 img cv2.imread(img_path) h, w img.shape[:2] info parse_ccpd_filename(os.path.basename(img_path)) lt_x, lt_y, rb_x, rb_y info[bbox] # 转成中心点 宽高 归一化 box_w rb_x - lt_x box_h rb_y - lt_y x_c lt_x box_w / 2.0 y_c lt_y box_h / 2.0 x_c_norm, y_c_norm x_c / w, y_c / h w_norm, h_norm box_w / w, box_h / h # 写入YOLO标签文件类别ID固定为0只有一个plate类 label_path os.path.join(save_dir, tag) with open(label_path, w) as f: f.write(f0 {x_c_norm:.6f} {y_c_norm:.6f} {w_norm:.6f} {h_norm:.6f}\n)转换脚本里有一处需要额外注意box_w和box_h必须用rb_x - lt_x这种差值计算不能直接拿角点坐标当宽高用。我刚上手时把rb_x误当成宽度结果训练了20个epoch后画出来的标注框全是横向拉长的矩形最后排查到是这里写错了。数据划分按常见做法保持9:1并且固定随机种子。训练集和验证集要从目录维度打散不要按文件名首字母排序后直接切前90%那样会让同段道路拍摄的图片全部挤在训练集里验证集完全失去意义。建议在生成标签文件的循环里加一个random.random() 0.9判断边转换边决定是写入train列表还是val列表。3. YOLOv5检测端训练plate.yaml、超参数选择与结果判读3.1 配置plate.yaml数据准备好之后先在YOLOv5工程目录下新建plate.yaml。这个文件告诉训练脚本去哪里读图片、有几类目标、类名是什么。资源里已经配好了plate.yaml你只需要确认里面的路径和你转换后的目录结构对得上。# plate.yaml path: ../ccpd_format # 数据根目录 train: images/train # 训练图片相对路径 val: images/val # 验证图片相对路径 nc: 1 # 类别数这里只有plate一个类 names: [plate] # 类别名建议用英文防止绘图时中文乱码三个字段缺一不可path是相对于你执行训练命令的目录来写的如果你的数据放在YOLOv5工程外面写成绝对路径更省事train和val指向的是图片目录而不是标签目录YOLOv5会按同名规则自动去找与图片同名的txt标签文件所以标签目录结构必须和图片目录完全一致。nc为1代表只做单类检测这个项目不需要区分蓝牌和黄牌都归为plate即可。3.2 训练命令与超参数选择训练命令使用YOLOv5官方train.py权重建议用yolov5s.pt起步s版本在精度和推理速度之间最均衡。对GPU显存小于8G的环境如果跑不动640分辨率可以把--img降到512但代价是对小目标的召回率会明显下降因为CCPD里车牌宽高往往只有整图的十几分之一。python train.py \ --data plate.yaml \ --weights yolov5s.pt \ --img 640 \ --batch-size 16 \ --epochs 100 \ --hyp data/hyps/hyp.scratch-low.yaml \ --project runs/plate_train这里有几个参数值得单独调。--hyp默认很多人不设但车牌检测属于单类别小目标场景我一般会用hyp.scratch-low.yaml而不是high。high配置里有更强的色彩抖动和随机仿射变换对小目标检测来说过强的mosaic数据增强会把车牌裁切到很小的局部块反而让模型学到错误的上下文特征。low配置保留基本增强收敛更稳定CCPD这种百万级图片规模的数据集不需要靠极端增强来凑数量。yolov5超参数文件里的lr0初始学习率是另一个关键点。hyp.scratch-low.yaml默认lr0: 0.01配合batch-size 16不需要手动调整如果你把batch-size加到32或64学习率要做同步缩放常见做法是按lr * batch / 16等比放大。不改学习率硬上大batch训练前期loss会下降得特别慢。参数推荐值说明--img640车牌是小目标太小丢失特征太大显存不够--batch-size168G显存的安全值16G可以上32--epochs100车牌单类目标50轮基本收敛100轮留足余量--hyplow弱增强更适合小目标检测--weightsyolov5s.pt用COCO预训练权重做迁移学习收敛更快训练启动后重点不是盯着终端刷epoch而是去看runs/plate_train/exp/目录下生成的文件。train_batch0.jpg和val_batch0_labels.jpg这两张图会展示前几个batch的标注框叠加效果如果这里框和车牌对不上说明上一章的坐标转换脚本写错了这时应该立刻CtrlC去查解析逻辑而不是继续等100个epoch训完。3.3 从results.csv判读训练状态每轮训练结束后YOLOv5会把所有关键指标写入runs/plate_train/exp/results.csv。用下面的命令可以实时看损失下降趋势tail -n 20 runs/plate_train/exp/results.csv正常收敛过程中train/box_loss和train/obj_loss应该在前30个epoch内从0.07左右降到0.03以下metrics/mAP0.5逐步逼近0.95以上。如果box_loss震荡不降优先检查标签是否出现大面积为0的归一化值——CCPD文件名解析时如果宽高算成负数写入txt的坐标值就会越界YOLOv5不会报错但loss永远不收敛。YOLOv5的backbone默认采用CSPNet结构它在残差连接的基础上做了梯度截断让CNN在深层网络中仍能保持较强的特征学习能力。对车牌这种目标小、纹理边界清晰的任务CSPNet在浅层就能捕获到车牌边缘的强响应所以即便只用s版本精度也足够应对CCPD的复杂背景。等这一轮训练结束runs/plate_train/exp/weights/best.pt就是后续推理使用的最终权重。4. CNN识别端saved_model加载、预处理与字符解码4.1 为什么车牌字符识别要单独接CNNYOLOv5检测端输出的只是一组坐标框它只回答“车牌在哪里”不回答“车牌号是多少”。要把框里的内容变成文本需要第二个模型。车牌识别本质上是一个定长的序列分类问题大陆车牌固定7个字符第一位是省份简称汉字后面6位是字母和数字这种强结构非常适合用CNN卷积神经网络做整图多标签分类。那为什么不直接把YOLOv5的分类头扩展成车牌子类别因为定位和字符识别是两种粒度不同的问题。YOLOv5负责在整张场景图中找到目标它的分类能力更多体现在“区分车和背景”上而车牌识别要把70x30左右的小图细分出31个省份汉字加34个字母数字的差异这需要专门的纹理特征提取网络。两级串接可以把检测和识别分开调优检测召回率不够就加数据识别精度不够就换识别网络互不干扰这是实际工程里更稳妥的做法也方便答辩时单独讲每一段的改进空间。4.2 加载plateRec_model并确定输入尺寸资源里的plateRec_model保存的是TensorFlow SavedModel格式目录下带有keras_metadata.pb和variables文件夹说明模型是用tf.keras导出的。加载方式如下import tensorflow as tf # 加载整个saved_model目录 model tf.keras.models.load_model(plateRec_model) # 打印输入输出张量的shape确认预处理尺寸 print(input shape:, model.inputs[0].shape) print(output shape:, model.outputs[0].shape)model.inputs[0].shape是(1, height, width, channels)或者(None, height, width, channels)常见的车牌识别网络输入是(70, 30, 3)或(168, 48, 3)这个数量级的尺寸。model.outputs[0].shape如果是(None, 7, 65)说明网络直接输出7个字符各自的分类概率如果看到的是(None, 455)那是7乘65摊平后的结果后面需要reshape。拿到输入尺寸后写一个通用的预处理函数让resize目标尺寸从模型中动态读取而不是硬编码。这样换模型时不需要改代码def preprocess_plate_crop(crop_bgr): # crop_bgr: YOLOv5裁剪出的车牌BGR图 h, w model.inputs[0].shape[1], model.inputs[0].shape[2] resized cv2.resize(crop_bgr, (w, h)) # 注意是(w, h)顺序 rgb cv2.cvtColor(resized, cv2.COLOR_BGR2RGB) # BGR转RGB normalized rgb.astype(float32) / 255.0 # 归一化到[0,1] return tf.expand_dims(normalized, axis0) # 增加batch维度这里有一个高频率踩坑点cv2.resize的第二个参数是(width, height)而model.inputs[0].shape返回的顺序是(height, width, channels)直接拿shape[1]和shape[2]去resize会正好把宽高对调。我习惯先把h和w单独取出来再按(w, h)传给resize避免这个低级错误。4.3 字符映射与plate_dec.py解码plate_dec.py在项目里的作用是把模型输出的概率向量还原成车牌字符串。它内部维护一张索引到字符的映射表常见结构是# 省份简称索引从35开始前0~9是数字10~34是字母 PROVINCES [京, 津, 冀, 晋, 蒙, 辽, 吉, 黑, 沪, 苏, 浙, 皖, 闽, 赣, 鲁, 豫, 鄂, 湘, 粤, 桂, 琼, 渝, 川, 贵, 云, 藏, 陕, 甘, 青, 宁, 新] LETTERS ABCDEFGHJKLMNPQRSTUVWXYZ # 部分版本不含I和O DIGITS 0123456789 def idx_to_char(idx): if idx 10: return DIGITS[idx] if idx 10 len(LETTERS): return LETTERS[idx - 10] return PROVINCES[idx - 10 - len(LETTERS)]解码的完整逻辑是模型输出先做一次argmax拿到7个字符各自的索引然后逐位查表拼成字符串。如果模型输出层是摊平结构需要先reshape成(7, num_classes)再做argmaxdef decode_plate(output_logits): import numpy as np if len(output_logits.shape) 2: # (1, 455) - reshape成7个字符 output_logits output_logits.reshape(1, 7, -1) pred_idx np.argmax(output_logits, axis-1) # 每个字符取概率最大的索引 plate .join(idx_to_char(i) for i in pred_idx[0]) return plate整图识别比字符分割方案更好的地方在于它不需要先做字符定位。车牌字符之间本身有间隙但受倾斜、反光和污损影响字符分割经常把“1”和“I”切断或者把“2”和“Z”黏在一起。CNN整图分类通过卷积核感受野自动覆盖到邻近字符的上下文端到端优化下来的鲁棒性明显优于先分割后分类的传统流程。5. 端到端串接从YOLOv5的box到最终车牌输出与精度统计5.1 把检测和识别串成一个函数现在把YOLOv5的检测输出和CNN识别模型串成完整链路。项目里的plate_rec.py做的就是这件事下面是它的核心骨架import cv2 import torch def detect_and_recognize(img_path, detect_model, rec_model, conf_thresh0.4): # 读图并做YOLOv5推理 img_bgr cv2.imread(img_path) results detect_model(img_bgr) # 返回检测结果对象 boxes results.xyxy[0].cpu().numpy() # 每行: x1, y1, x2, y2, conf, cls plates [] for box in boxes: x1, y1, x2, y2, conf, cls box if conf conf_thresh: continue # 用安全距离裁剪车牌区域防止边缘字符被切掉 x1 max(0, int(x1 - 2)); y1 max(0, int(y1 - 2)) x2 min(img_bgr.shape[1], int(x2 2)); y2 min(img_bgr.shape[0], int(y2 2)) crop img_bgr[y1:y2, x1:x2] input_tensor preprocess_plate_crop(crop) # 复用第4章的预处理 output_logits rec_model.predict(input_tensor, verbose0) plate_text decode_plate(output_logits) # 复用第4章的解码函数 plates.append({box: (x1, y1, x2, y2), text: plate_text, conf: float(conf)}) return plates这里有一个我在串接时踩过的坑YOLOv5裁剪出的车牌图经常把左右边缘的字符贴边截掉一半导致识别端把“1”误判成“I”。解决办法是在裁剪时向外扩展2到3个像素的安全边距让CNN能看到完整的字符轮廓。注意扩展后要做边界截断否则坐标会越界。5.2 用CCPD文件名标签做精度统计CCPD的最大便利是每张图的车牌号可以从文件名解析出来这正好用来做自动化评估。批量跑完检测后把结果写入CSV同时和文件名标签比对统计检测率和识别率。import csv import os def evaluate_on_dataset(image_dir, detect_model, rec_model): rows [] for img_name in os.listdir(image_dir): if not img_name.endswith(.jpg): continue info parse_ccpd_filename(img_name) # 第2章的解析函数 true_plate .join(idx_to_char(int(i)) for i in info[plate_idx]) pred_plates detect_and_recognize( os.path.join(image_dir, img_name), detect_model, rec_model) # 只取置信度最高的检测框作为预测结果 hit any(p[text] true_plate for p in pred_plates) rows.append([img_name, true_plate, pred_plates[0][text] if pred_plates else , pred_plates[0][conf] if pred_plates else 0.0, int(hit)]) with open(eval_results.csv, w, newline) as f: writer csv.writer(f) writer.writerow([image, true_plate, pred_plate, conf, correct]) writer.writerows(rows) total len(rows) correct sum(r[4] for r in rows) print(f识别准确率: {correct / total:.4f} ({correct}/{total}))把correct定义为整字符串完全匹配而不是逐字符匹配是刻意为之。答辩时如果只报逐字符准确率评委追问“整牌准确率多少”就会露怯反过来整牌准确率能过逐字符准确率一定更高从整牌指标反推逐字符指标也更符合评价习惯。5.3 用CCPD的模糊度字段定位识别瓶颈如果整体准确率只有90%别急着调模型先用CCPD文件名第7段的模糊度字段做一次分层统计。这个字段的值越大代表图片越模糊按0-5、6-10、11-20、20以上分四档分别跑一次识别率通常能看到一条明显的下降曲线。def group_by_blur(eval_csv): groups {0-5: [0, 0], 6-10: [0, 0], 11-20: [0, 0], 21: [0, 0]} with open(eval_csv) as f: for row in csv.DictReader(f): blur int(parse_ccpd_filename(row[image])[blur]) if blur 5: key 0-5 elif blur 10: key 6-10 elif blur 20: key 11-20 else: key 21 groups[key][0] int(row[correct]) groups[key][1] 1 for k, (corr, total) in groups.items(): print(fblur {k}: {corr / total:.4f} ({corr}/{total}))这个统计能直接回答答辩老师最爱问的“你的模型在什么情况下会失效”。如果发现高模糊档位准确率骤降说明瓶颈在识别端优先尝试对裁剪图做锐化预处理如果所有档位都有大量漏检说明瓶颈在检测端去调YOLOv5的置信度阈值或换成更大分辨率的输入。把改进前后的分层统计放在一起对比比单贴一张效果图有说服力得多。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门