简介:本资源面向计算机视觉方向的毕业设计、课程设计及学科竞赛参与者,提供一套基于CNN与YOLOv5的车牌检测与识别完整工程,数据集采用CCPD官方数据集,可帮助读者快速搭建车牌识别实验环境并完成项目复现。压缩包共10个文件,约44.33MB,包含Python脚本、模型权重文件、YAML配置、Keras模型文件、Jupyter Notebook及README说明文档,覆盖训练、检测、识别等核心环节,便于按模块查阅与二次开发。目前已有78人学习下载,适合具备一定深度学习基础、需要完整项目参考或扩展功能的学习者。资源内代码经过测试运行,功能正常,可复现复刻,设计报告亦可借鉴,并支持在此基础上修改以实现其他功能。作者具备全栈开发经验,使用中遇到问题可随时联系获取解答与相关开发工具、学习资料,鼓励学习进步。
1. 车牌检测识别为什么总在“最后一公里”翻车
停车场闸机抬杆慢半拍、高速卡口抓拍糊成一片、地库暗光下把“京A”认成“京B”——这些场景做过的都懂。车牌检测和识别看着是经典任务,真落地时却总在最后一公里翻车:检测框抖、字符粘连、倾斜角度一大就崩。标题里这套方案用 CNN 做字符识别、YOLOv5 做车牌定位,数据集直接上 CCPD 官方数据集,本质是把“检测”和“识别”拆成两段可控的流水线。它适合谁?做毕设课设的学生、要快速搭车牌 demo 的工程师、以及想拿一个完整视觉 pipeline 练手的人。CCPD 是国内车牌场景里标注质量最扎实的公开数据集之一,配合 YOLOv5 的工程成熟度,能让你把精力放在调参和踩坑上,而不是从零标数据。这一章先把“为什么这么搭”讲清楚,后面直接上代码和参数。
2. 拆解流水线:YOLOv5 定位车牌,CNN 识别字符
2.1 为什么不是端到端一个模型搞定
很多人第一反应是搞一个端到端模型,输入整图直接输出车牌字符串。想法很美好,实操很骨感。车牌在整图里占比极小,端到端模型要同时学“在哪”和“是什么”,训练时梯度会被背景噪声稀释,收敛慢且容易过拟合。更现实的问题是:CCPD 的标注是四点坐标加车牌号,端到端需要你把字符级位置也标出来,工作量翻倍。
拆成两段就舒服多了。第一段 YOLOv5 只干一件事:把车牌从整图里框出来,输出裁剪后的车牌小图。第二段 CNN 只干一件事:对规整后的小图做字符分类。两段各自的数据分布更集中,YOLOv5 学“车牌长什么样”,CNN 学“字符长什么样”,互不干扰。常见做法是检测用 YOLOv5s 或 YOLOv5m,识别用一个小型 CNN(比如 4 层卷积加两层全连接),整体推理在 GPU 上能压到 20ms 以内。
提示:如果你的场景车牌种类单一(比如只有蓝牌),检测模型可以更小;如果蓝牌、绿牌、黄牌混在一起,YOLOv5m 的容量更稳。
2.2 CCPD 数据集的结构与转换脚本
CCPD 官方数据集的文件名本身就是标注,这是它最方便也最容易踩坑的地方。每张图文件名形如025-95_113-226&469_448&489-440&489_232&469_226&469_440&469-0-0.jpg,用-分段后:第 3 段是车牌四个角点坐标,第 5 段是车牌号,第 7 段是亮度,第 8 段是模糊度。你得先写脚本把它转成 YOLO 需要的 txt 格式。
import os import cv2 import numpy as np def ccpd_to_yolo(img_dir, out_dir): os.makedirs(out_dir, exist_ok=True) for fname in os.listdir(img_dir): if not fname.endswith('.jpg'): continue parts = fname.split('-') # 第3段是四个角点,格式 x1&y1_x2&y2_x3&y3_x4&y4 points = parts[2].split('_') coords = [] for p in points: x, y = p.split('&') coords.append((int(x), int(y))) xs = [c[0] for c in coords] ys = [c[1] for c in coords] x_min, x_max = min(xs), max(xs) y_min, y_max = min(ys), max(ys) img_path = os.path.join(img_dir, fname) img = cv2.imread(img_path) h, w = img.shape[:2] # YOLO 格式:class cx cy bw bh,全部归一化 cx = (x_min + x_max) / 2.0 / w cy = (y_min + y_max) / 2.0 / h bw = (x_max - x_min) / w bh = (y_max - y_min) / h label_path = os.path.join(out_dir, fname.replace('.jpg', '.txt')) with open(label_path, 'w') as f: f.write(f"0 {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n") if __name__ == '__main__': ccpd_to_yolo('./ccpd/images', './ccpd/labels')这段脚本的逻辑很直白:从文件名解析四点坐标,取外接矩形,再按 YOLO 要求归一化。参数上注意class固定为 0,因为只有“车牌”一类。cx cy bw bh全部除以图像宽高,这是 YOLOv5 的硬性要求,忘了归一化训练时 loss 会直接 NaN。跑完检查一下 labels 目录,txt 数量和图片数量应该一致,少一张都说明有文件解析失败。
2.3 YOLOv5 训练配置与三个必调参数
数据转好后,YOLOv5 的训练入口是train.py。我一般会先建一个data/ccpd.yaml,内容如下:
path: ./ccpd train: images/train val: images/val nc: 1 names: ['plate']然后跑训练命令:
python train.py --data data/ccpd.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 100 --name ccpd_exp三个必调参数:--img决定输入分辨率,CCPD 里车牌普遍偏小,640 是底线,显存够就上 1280;--batch看显存,16 是 8G 卡的稳妥值;--epochs别迷信 300,CCPD 数据量大,100 轮后 mAP 基本就平了,再训就是浪费电。训练完看runs/train/ccpd_exp/weights/best.pt,这个权重后面识别阶段还要用。
3. 字符识别 CNN 的搭建与训练细节
3.1 车牌字符分类的 CNN 结构设计
检测框出来之后,把车牌小图裁出来,做灰度化和尺寸归一化(常见做法是 94x24 或 120x30),然后送进 CNN。车牌字符集是固定的:省份简称、字母、数字,一共 65 类左右(不含特殊车牌)。所以这本质上是一个 65 分类问题,不需要太深的网络。
import torch import torch.nn as nn class PlateCNN(nn.Module): def __init__(self, num_classes=65): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(128, 128, 3, padding=1), nn.ReLU() ) self.classifier = nn.Sequential( nn.AdaptiveAvgPool2d((1, 6)), # 保留水平方向,适配字符序列 nn.Flatten(), nn.Linear(128 * 6, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, num_classes) ) def forward(self, x): x = self.features(x) return self.classifier(x)这里的关键设计是AdaptiveAvgPool2d((1, 6))。车牌是 7 个字符水平排列,直接全局池化会丢掉位置信息,保留 6 到 7 个水平槽位能让网络学到“第几个字符是什么”。Dropout(0.3)是防过拟合的后悔药,CCPD 里某些省份样本少,不加 dropout 容易在稀有省份上翻车。
3.2 从检测框到字符标签的样本生成
训练 CNN 需要“车牌小图 + 对应字符串”的样本对。CCPD 文件名里第 5 段就是车牌号,直接拿来当标签。但要注意:文件名里的车牌号是完整字符串,而 CNN 是逐字符分类,所以你得把字符串拆成字符列表,再映射到索引。
CHARS = "京沪津渝冀晋蒙辽吉黑苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新" LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" DIGITS = "0123456789" CHARSET = CHARS + LETTERS + DIGITS char_to_idx = {c: i for i, c in enumerate(CHARSET)} def parse_plate_number(fname): parts = fname.split('-') plate = parts[4] # 第5段是车牌号 return [char_to_idx[c] for c in plate if c in char_to_idx]参数说明:CHARSET的顺序一旦定下就不能改,训练和推理必须用同一套映射,否则识别结果会整体错位。parse_plate_number里过滤了不在字符集里的字符,防止文件名里有异常符号导致 KeyError。生成样本时,用 YOLOv5 的检测框裁图,再 resize 到 94x24,存成 npy 或直接在线裁都行。我一般离线裁好,训练时直接读,省得每个 epoch 都跑一遍检测。
3.3 训练循环与验证指标
CNN 的训练循环没什么玄学,交叉熵损失加 Adam 就够。但验证指标别只看 accuracy,车牌识别要看“整牌准确率”——7 个字符全对才算对。
def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss = 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() return total_loss / len(loader) def eval_plate_acc(model, loader, device): model.eval() correct, total = 0, 0 with torch.no_grad(): for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) preds = model(imgs).argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) return correct / total注意eval_plate_acc这里算的是字符级准确率,整牌准确率需要你把同一张图的 7 个字符预测拼起来再比对。实操中字符级能到 99% 时,整牌往往只有 90% 出头,差距就在那些模糊和倾斜样本上。
4. 检测与识别联调:从整图到车牌字符串
4.1 推理管道的组装顺序
联调时顺序很重要:先检测,再裁图,再识别。别反过来。检测用best.pt,识别用训练好的 CNN 权重。整条管道写成一个函数,输入是原始 BGR 图像,输出是车牌字符串列表。
def detect_and_recognize(img, yolo_model, cnn_model, device): results = yolo_model(img, size=640) detections = results.pandas().xyxy[0] plates = [] for _, row in detections.iterrows(): if row['confidence'] < 0.5: continue x1, y1, x2, y2 = int(row['xmin']), int(row['ymin']), int(row['xmax']), int(row['ymax']) crop = img[y1:y2, x1:x2] gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) gray = cv2.resize(gray, (94, 24)) tensor = torch.from_numpy(gray).float().unsqueeze(0).unsqueeze(0) / 255.0 tensor = tensor.to(device) with torch.no_grad(): preds = cnn_model(tensor).argmax(dim=1) plate_str = ''.join([CHARSET[i] for i in preds.cpu().numpy()]) plates.append(plate_str) return plates参数上confidence阈值 0.5 是起点,实际场景如果漏检多就降到 0.3,误检多就升到 0.6。resize的目标尺寸必须和训练 CNN 时一致,不一致的话识别率会断崖式下跌,这是血泪经验。
4.2 检测框微调对识别率的影响
YOLOv5 输出的框有时候会偏紧或偏松。偏紧会切掉字符边缘,偏松会带进背景噪声。我一般会在裁图时把框向外扩 5% 到 10%,给字符留点余量。
def expand_box(x1, y1, x2, y2, ratio=0.08): w, h = x2 - x1, y2 - y1 dx, dy = int(w * ratio), int(h * ratio) return max(0, x1 - dx), max(0, y1 - dy), x2 + dx, y2 + dy这个ratio别设太大,0.15 以上容易把相邻车牌框进来。实测 0.08 到 0.12 之间比较稳。另外如果车牌倾斜严重,裁出来的图会有黑边,可以在 resize 前做一次透视校正,但那是另一个话题了。
4.3 批量推理与结果落盘
实际跑测试集时,别一张一张喂,用 DataLoader 批量推理能快好几倍。结果落盘建议存成 json,每张图对应一个车牌列表,方便后面算指标。
import json def batch_inference(img_list, yolo_model, cnn_model, device, out_json): all_results = {} for img_path in img_list: img = cv2.imread(img_path) plates = detect_and_recognize(img, yolo_model, cnn_model, device) all_results[img_path] = plates with open(out_json, 'w', encoding='utf-8') as f: json.dump(all_results, f, ensure_ascii=False, indent=2)ensure_ascii=False是为了让中文省份简称正常显示,不加的话会变成 unicode 转义,后面读起来费劲。
5. 避坑与排查:车牌检测识别最常见的五个翻车点
5.1 现象:训练 loss 正常但 mAP 一直是 0
原因:CCPD 转换后的 label 文件里坐标没归一化,或者归一化时除了错误的宽高。YOLOv5 对坐标范围极其敏感,超出 [0,1] 直接不参与计算。
解决:写个检查脚本,遍历所有 txt,确认四个值都在 0 到 1 之间。发现异常就回查转换脚本里w和h是不是取反了。
5.2 现象:识别结果整体偏移一位,比如“京A12345”认成“A12345京”
原因:CNN 训练时字符顺序和推理时拼接顺序不一致,或者AdaptiveAvgPool2d的水平槽位和实际字符数对不上。
解决:固定CHARSET顺序,训练和推理共用同一个映射文件。池化槽位设成 7 而不是 6,给每个字符一个独立槽位。
5.3 现象:地库暗光图片检测框抖动严重
原因:YOLOv5 在低照度下特征响应弱,框的回归不稳定。CCPD 里虽然有亮度标注,但训练时没做针对性增强。
解决:训练时加--augment,并在数据加载里对暗光样本做 gamma 校正。推理时如果场景固定,可以先把图提亮再送检测。
5.4 现象:绿牌新能源车牌识别率明显低于蓝牌
原因:CCPD 里蓝牌占比远高于绿牌,类别不平衡导致 CNN 对绿牌字符欠拟合。
解决:对绿牌样本做过采样,或者在 loss 里给绿牌字符更高权重。简单做法是复制绿牌样本到训练集里,让比例接近 1:1。
5.5 现象:推理速度慢,单张图超过 200ms
原因:YOLOv5 用了大模型(比如 yolov5x),或者 CNN 没跑在 GPU 上。
解决:检测换 yolov5s,CNN 确认.to(device)了。另外把torch.no_grad()加上,不然会白算梯度。
6. 进阶技巧:用检测置信度过滤 + 识别置信度重排
前面讲的管道是“检测框直接裁图送识别”,但实际场景里会有误检框,比如把广告牌上的文字框成车牌。一个实用的进阶技巧是:CNN 输出 softmax 概率后,取 7 个字符概率的乘积作为整牌置信度,低于阈值的直接丢弃。
def plate_confidence(logits): probs = torch.softmax(logits, dim=1) top_probs, _ = probs.max(dim=1) return top_probs.prod().item()这个乘积会偏小,因为 7 个概率相乘,每个 0.9 的话乘积只有 0.48。所以阈值别设太高,0.3 左右比较合适。另一个技巧是:如果同一张图检测出多个框且重叠度高,用 NMS 去重后再识别,避免同一车牌出多个结果。
验证方法上,我习惯把测试集分成“正常光照”“暗光”“倾斜”三组,分别算整牌准确率。正常组能到 95% 以上,暗光组往往掉到 80% 左右,倾斜组看角度,30 度以内还行,超过 45 度基本就得加校正模块了。这套方案值不值得做?如果你要的是一个能跑通、能改、能写进毕设的完整 pipeline,它足够扎实;如果你追求工业级 99.9% 的识别率,那还得在数据增强和校正上继续磨。我自己的习惯是先把 baseline 跑通,再针对最差的那组数据做定向优化,别一上来就堆模型。希望帮到你。
本文还有配套的精品资源,点击获取