基于深度学习的车牌识别系统完整工程:YOLO检测+LPRNet字符识别实践
简介一套基于深度学习的端到端车牌识别系统完整方案面向人工智能与计算机视觉方向的课程设计和毕业设计。整体方法上采用YOLO框架完成车牌实时检测用LPRNet和STNet处理字符识别与图像矫正并考虑树莓派等边缘设备的部署场景适合想要完整走通算法训练、模型评估到落地部署流程的学习者。压缩包共一百零四个文件大小约七点五五兆字节包含三十七个Python源码、四十一个YAML配置、两个PyTorch权重以及容器部署文件、shell部署脚本、markdown说明文档等。其中深度学习基础文档和树莓派配置指南分别讲解理论背景与现场安装步骤教程notebook便于逐步复现整体目录结构清晰便于使用者快速定位。目前已有四十九人学习浏览。压缩包内直接提供可运行的识别代码、预训练权重、跨平台部署方案及配套资料既适合毕业设计演示也能作为进一步研究目标检测与车牌字符识别的实践起点。1. 车牌识别系统.zip 里装了什么一个有检测有识别的完整落地工程拿到的《基于深度学习的车牌识别系统.zip》解压后不是一堆零散代码而是一条完整可跑的深度学习落地链路YOLO 负责在整图中定位车牌位置序列识别网络负责从车牌图块里读出字符两者拼起来就是一套从原始图片到最终车牌号的识别系统。对深度学习课程设计或者毕业设计来说这个工程的价值不在于模型多新而在于数据转换、模型训练、指标验证这几块都齐全你不会卡在某一环。它适合三种人想用 YOLO 做一次完整 CV 交付的初学者需要一套能在答辩现场跑出结果的毕业生以及想复现“检测识别”经典两段式任务的从业者。先给大家一个判断标准如果你只是要一个实时摄像头识别 demo这套东西稍作后端封装就能用如果你以为解开压缩包就能零配置跑通那先把第 3 章的数据格式检查看完再动手。2. 工程结构先于代码检测识别两段式为什么比端到端更好用拿到压缩包第一件事不是找 README而是先看目录。车牌识别这类 CV 工程有个共同点代码骨架比较固定变化的是数据、权重和字符集。先把目录读明白后面每一步都知道在改哪里。2.1 解压后先看目录不用 README 也能反向读懂工程这个 zip 解压后功能模块大体和下面的结构对应不同发布版本里文件夹名字可能略有出入但套路一致plate_recognition/ ├── data/ │ ├── images/ # 全部原图通常按 train/val 分好 │ ├── labels/ # 检测模型的 YOLO 格式标签txt │ └── plate.yaml # 检测训练用的数据配置 ├── models/ │ ├── detector/ # YOLOv5 检测源码 │ └── recognizer/ # LPRNet 类的字符识别网络 ├── weights/ │ ├── plate_detect_best.pt │ └── plate_ocr.pth ├── utils/ │ ├── voc2yolo.py # VOC xml 标注转 YOLO txt │ └── split_data.py # 训练集/验证集划分 └── docs/ └── 使用说明.md从结构就能读出这个工程的调度流程先跑 utils 里的转换脚本整理数据再用 models/detector 训练检测器用 models/recognizer 训练识别器最终推理时两个权重串行调用。看目录比看 README 更可靠因为 README 可能是过时的而文件夹里实际放着什么不会骗人。主推的顺序是数据 → 检测 → 识别 → 推理这个顺序也是训练正确性的依赖顺序。2.2 两段式的原理检测负责定位识别负责分类先回答一个初学者最容易问的问题为什么不训练一个网络直接输入图片输出车牌号端到端方案理论上可行但实际维护成本高。车牌识别是一个目标小、要求精确的多字符分类任务如果网络一步到位回归出 7 到 8 个字符网络必须同时学习“找到车牌”和“区分字符”两种能力任何一类数据不足都会互相拖累。拆成两段后第一段 YOLO 只需要回答一个问题图像里有没有车牌在哪里。这是一个单类别目标检测任务几千张标注图就能训练到可用水平。第二段拿到的是已经裁剪好的车牌图块网络只需要做序列识别比如把 94x24 的车牌图片映射成“粤B·12345”这样的字符串。两个模型的数据集小、单点更新容易这是工程上更稳的做法。选 YOLOv5s 而不是更大的 YOLOv5m 或更新的版本原因有三一是 s 模型参数量约 7MGTX 1060 都能跑实时推理二是车牌只有单类目标用不到更大模型容量三是课程设计和毕业设计资料里 YOLOv5 工具链最全报错随便一搜都有答案。如果你手头显存只有 4G换成 YOLOv5n 也能跑精度损失在这个任务里并不明显。2.3 输入输出约定尺寸、通道、字符集一个都不能乱两段式工程里真正决定能不能跑通的是模块之间的接口约定。这个系列的接口可以用一张表概括模块输入输出训练数据规模参考检测YOLOv5s整图建议 640x640RGB车牌 bbox 坐标 置信度300010000 张识别LPRNet车牌 ROI统一 32x140灰度或 RGB字符序列长度不固定2000050000 张注意识别网络的输入宽度是固定的因为 LPRNet 内部有循环层它接收的是定宽的序列特征所以车牌图块在传入前必须做等比例缩放再 padding 成 32x140。字符长度不固定是因为车牌有 7 位蓝牌和 8 位新能源绿牌的区别识别输出用 CTC 解码而不是固定 7 分类。通道方面检测模型普遍用 RGB 三通道预训练权重识别模型很多公开实现直接用灰度图如果你的数据是彩色可以在训练识别网络时把彩色转灰度或者改成三通道输入并调整第一层卷积但别在推理时用彩图喂灰度训练的模型通道不一致会直接出 NaN。3. 数据对齐把 VOC 标注转成 YOLO 格式的脚本与训练集划分3.1 从 XML 标注到 TXT 标签为什么要加这一层转换很多公开数据集的标注是 Pascal VOC 格式即每张图对应一个 XML 文件记录对象名和 bndbox 四点坐标。而 YOLO 系列训练时只需要一个纯文本文件每行表示一个目标类别编号、中心点 x、中心点 y、宽度 w、高度 h全部归一化到 01 之间。两者之间必须有一层转换。为什么 YOLO 设计成这种格式因为归一化坐标不依赖图像分辨率训练时可以放心做随机缩放、拼接、裁剪等增强标签不会错位。而 VOC 的绝对坐标一旦图像被 resize对应关系就断了。所以不管下载的数据集是 xml 还是 json最终都会被转换成 txt 再进训练。3.2 转换脚本归一化坐标和类别映射是关键压缩包里一般会自带一个 voc2yolo.py逻辑和下面的等价如果没带抄这个也能用import xml.etree.ElementTree as ET import os # 只保留我们要检测的类别类名区分大小写 CLASS_NAMES [plate] def convert_one_xml(xml_path, out_path): tree ET.parse(xml_path) root tree.getroot() # 从 XML 头信息取原始图片宽高归一化必须依赖原始尺寸 img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) with open(out_path, w, encodingutf-8) as f: for obj in root.iter(object): name obj.find(name).text if name not in CLASS_NAMES: continue # 跳过非车牌对象避免污染标签 box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) # 归一化中心点和宽高全部除以原图宽高 cx ((x1 x2) / 2.0) / img_w cy ((y1 y2) / 2.0) / img_h bw (x2 - x1) / img_w bh (y2 - y1) / img_h # YOLO 坐标系是左上角原点v5/v8 通用 cls_id CLASS_NAMES.index(name) f.write(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}\n)注意一个高频错误坐标归一化必须使用 XML 头里记录的原始宽高而不是你在脚本里 cv2.imread 之后再取 shape。如果原图被预处理过比如自动旋转、去 EXIF 信息两边尺寸可能不一致标签会整体偏移。还有一个细节是类别编号从 0 开始CLASS_NAMES 里顺序固定后尽量不要变因为训练和推理用的是同一个 index。提示转换脚本跑完随机挑 5 张图把 txt 坐标画回原图对比一次。这一步只花五分钟能挡掉后面十几个小时的无效训练。3.3 训练集/验证集划分一个随机种子解决“图片和标签对不上”标注转完格式后还要把数据划分成 train 和 val。常见分歧是有人按文件夹划一半图片 train、一半图片 val实际上 YOLO 的 data.yaml 只认目录文件夹划分没有问题但在同一个文件夹里时需要用脚本生成清单。一个常见的划分脚本如下import os import random from glob import glob random.seed(42) # 固定种子保证每次重新划分的结果一致 img_dir data/images label_dir data/labels all_imgs sorted(glob(os.path.join(img_dir, *.jpg))) random.shuffle(all_imgs) # 按 9:1 划分 val_count max(1, int(len(all_imgs) * 0.1)) with open(data/train.txt, w) as ftrain, open(data/val.txt, w) as fval: for i, img_path in enumerate(all_imgs): if i val_count: fval.write(img_path \n) else: ftrain.write(img_path \n)这里最容易被忽略的是“图片和标签要同名同步移动”。如果你把某张图挪进 val 文件夹但 txt 标签没跟着动训练到一半就会报image without labels或者静默跳过导致训练集规模莫名其妙变小。我的做法是先固定随机种子然后用同一个 list 的下标同时分配图片和标签路径保证两边永远不会错位。还有一种极端情况是数据集里有一两张完全没有车牌的负样本这些负样本要在所有图片 shuffle 之后再插入训练集不能混进 val否则验证集的 mAP 会被拉低。划完数据之后记得看一眼各类样本量车牌数据集里蓝牌占 90%、绿牌占 5% 是很常见的失衡如果 val 里绿牌只有 3 张那验证指标说明不了真实水平。建议最少保留 1520 张每类的验证样本这个量级的统计才不至于出现 3 张全中就是 100% 的假象。4. 训练参数别全用默认值检测和识别各自的收敛关键很多人在这一步栽在同一个地方下载权重之后直接用默认参数训练发现 loss 不降就以为代码坏了。实际上 YOLO 默认参数是给 COCO 这种 80 类、目标大小分布广的数据集设计的放到“只有车牌一类、目标长宽比 3:1”的检测任务上至少有 3 个参数必须动。4.1 data.yaml 和 hyp.yaml类别、路径、增强参数的写法检测训练的第一步是把数据配置写对。data/plate.yaml 核心内容如下train: data/images/train val: data/images/val nc: 1 names: [plate]注意路径写法。YOLOv5 的 train 字段只能填图片目录标签目录会自动推断为同级 labels/。如果你把图片放在一个目录、标签放在另一个目录需要把两个目录的名字统一成 images 和 labels 这种约定或者手动改数据集代码里的路径变量。另一个常被忽略的点yaml 里缩进不规范会导致解析报错用空格别用 Tab。hyp 参数文件里我一般会把这几个项往大调hsv_h: 0.02 # 色相增强幅度蓝牌绿牌差异需要一定的颜色扰动 hsv_s: 0.6 # 饱和度增强绿牌在暗光下的辨识度靠它 hsv_v: 0.5 # 明度增强夜间样本少的补救手段 mosaic: 1.0 # 马赛克拼接增强目标小的时候建议保留车牌检测的场景里车速带来的运动模糊、逆光、夜间低照度是主要现实干扰。hsv 增强调高之后训练集能在不增加图片数量的情况下覆盖更多天气和光线变化。mosaic 建议保持开启车牌这种小目标在拼接图中会以更丰富的上下文出现但注意 mosaic 会放大标签错位风险如果前面数据转换时坐标不对mosaic 增强只会把错误放大得更明显。动手做过深度学习训练的人应该都有体会增强参数看着不起眼实际对最终精度的贡献往往比换模型还大。4.2 检测网络训练batch、epochs、img-size 与显存的关系训练命令通常长这样python train.py \ --data data/plate.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 80 \ --hyp data/hyp.scratch-low.yaml说说几个参数怎么定。img 640 是检测输入的边长车牌的长宽比大约 3:1如果缩到 320车牌的短边只剩十几个像素特征基本丢失所以不建议低于 512。batch 取决于显存6G 显卡用 812G 用 1624G 以上可以冲到 32。epochs 在车牌这种单类小数据上不需要 30080 到 100 轮足够因为 YOLOv5s 在 3000 张图上大概 30 轮就逼近收敛后面多数在震荡。跑之前先看一眼显卡显存batch 乘以训练图尺寸的乘积才是显存压力的来源而不是只看 batch 数字。训练效果有个快速核实方法训练完看runs/train/exp*/results.png。如果 val/box_loss 不再下降而 val/cls_loss 还在降说明检测框已经稳了可以提前停如果两个 loss 同时反弹多半是学习率没降下来把--cos-lr打开让学习率按余弦曲线衰减能解决大部分震荡问题。4.3 识别网络训练字符集决定上限CTC 决定长度识别模型通常是 LPRNet的训练比检测更容易被忽略因为数据集和代码不如 YOLO 那边显眼。字符集是整个识别系统里最重要的一个变量# 蓝牌新能源车牌常见字符集顺序不要随意变动 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, 京, 津, 冀, 晋, 蒙, 辽, 吉, 黑, 沪, 苏, 浙, 皖, 闽, 赣, 鲁, 豫, 鄂, 湘, 粤, 桂, 琼, 渝, 川, 贵, 云, 藏, 陕, 甘, 青, 宁, 新, ·, # 分隔符 _, # CTC 空白符很多实现里单独占一个位置 ]注意字母表里有意剔除了 I 和 O因为它们和数字 1、0 在车牌字体下几乎无法区分公开车牌数据集的标注通常也不包含这两个字符。字符集顺序一旦确定就不要改因为训练好的模型全连接层索引是对应这个顺序的识别模型重新训练时如果字符集变化必须连带输出层一起调整。识别模型的训练 loss 用 CTCConnectionist Temporal Classification它能处理“字符宽度不均、不需要预分割”的序列识别。CNN 部分负责提取车牌图片的序列特征双向 LSTM 对 32x140 的图块沿宽度方向逐步解码最终得到长度不固定的字符序列。训练时不要用固定 7 位长度的交叉熵因为蓝牌 7 位、绿牌 8 位长度本身是预测目标的一部分固定长度分类会直接限死模型能力。如果你拿到的资源里识别模型用的是固定 7 字符输出建议改成 CTC 分支这个改造是毕业设计答辩里最值得讲的一个加分点。5. 避坑记录五个真实翻车现场和处理办法5.1 中文车牌乱码字符表里根本没有“粤”“京”现象检测框完全正常车牌区域裁剪没问题但输出结果是??B·12345或者直接变成纯英文数字。检查原图时车牌汉字清楚可见不是图像质量问题。原因识别模型用的字符集只包含数字和英文训练数据里也没有省份汉字。很多公开的“车牌识别”小模型只识别 7 位字符中的后 6 位设计它的人把汉字那一列直接当作“省份省略”。课程设计里如果你要用它识别真实车牌就会拿到问号。解决把省份简称汉字补进 CHARS 列表找对应的汉字训练数据重训识别模型。如果你不想重训还有一个底线做法把检测框左上角的汉字单独切出来用一个小分类网络单独识别省份简称。这个方案能复用大部分已有权重新的汉字分类器只需要 31 类训练集要求不高。5.2 蓝牌效果正常、绿牌全翻车数据失衡和宽度归一化的问题现象蓝牌识别准确率 95% 以上新能源绿牌一下掉到 40%而且报错的多数是多一位字符或识别出乱码。原因两部分原因叠加。第一是数据失衡数据集里蓝牌占 80% 以上绿牌样本太少模型对绿色车牌的颜色分布根本没学出来第二是宽度归一化LPRNet 需要固定 32x140 输入而绿牌是 8 位字符比蓝牌长统一按宽度拉伸后字符被压扁CTC 解码就分不清相邻字符。解决对绿牌样本做过采样复制后叠加 hsv 增强让每张绿牌以多种颜色变化出现。宽度归一化不要直接 resize 成 140先按高度 32 等比缩放再在宽度方向 padding 到 140字符的宽高比就不会被破坏。如果原图车牌倾斜明显先做透视矫正再送识别效果会再上一个台阶。5.3 loss 不再下降mAP 卡在 0.7没重算 anchors现象训练到 40 轮以后loss 曲线逐渐走平mAP 在 0.7 附近上下抖动换更大模型也没改善。看检测结果小车牌经常被漏检或者框的大小明显偏大。原因默认 anchors 是从 COCO 数据集聚类出来的覆盖的是“人、车、猫狗”这类常规宽高比。车牌宽高比接近 3:1和默认 anchors 的匹配度差YOLO 在训练初期就把很多目标判成了背景。解决训练前单独跑一次 k-means 重聚类YOLOv5 训练时会自动用新先验框。实操上可以在训练命令里加--evolve让它自动搜索也可以用 utils 里的聚类脚本算好 6 组 anchor 写进 yaml。重算之后通常第 10 轮就能看到 mAP 明显拉升。5.4 CUDA out of memorybatch 和 img-size 的组合问题现象运行训练命令后几秒内直接爆显存报错CUDA out of memory。有人换成 batch 4 还是爆以为代码有问题。原因显存占用不只是 batch 决定imgsz 的平方才占大头。640x640 的输入比 512x512 多 56% 的显存占用再加上 mosaic 增强会在训练中途拼出更大的图片短时峰值会远超预期。解决先把 img 降到 512batch 设 8再跑一次看峰值。还不够就把 mosaic 在最后 10 轮关掉YOLOv5 里有 close_mosaic 参数因为 mosaic 拼图阶段才是显存尖峰。如果显存只有 4G建议直接用 YOLOv5n 替换 s 权重n 模型显存不到 s 的一半精度损失在单类车牌任务里基本可接受。5.5 车牌多一位少一位固定长度输出和 CTC 解码的选择现象有的车牌识别结果显示“粤B·12345”变成“粤·B12345”或者蓝牌 7 位输出 8 位最后一个字符是重复的前面某个字符。原因识别模型如果用固定 7 位全连接输出就必须在预处理时把车牌图块强行对齐到 7 个位置倾斜、逆光、字符间距不均都会让对齐失败如果用 CTC 但后处理没合并重复字符也会出现连续相同字符被重复输出的情况。解决统一改用 CTC 解码后处理时把“相邻同字符合并”和“去除空白符”两个步骤分开做。还有一个经验不要对识别结果直接 strip 空白有些省份简称本身包含类似空白间隔的字符。建议在字符集里单独定义分隔符“·”输出时用它的位置把省份汉字和后面数字分开这个细节在答辩演示时很能体现工程能力。6. 推理验证的验收动作从权重文件到一张图的完整输出6.1 推理脚本把检测框和字符输出串成一句话训练完两个模型最后一步是把检测和识别串起来跑一张完整图片。一个最小的推理脚本如下import cv2 import torch from models.experimental import attempt_load # 检测模型加载自定义权重路径用相对路径即可 detector attempt_load(weights/plate_detect_best.pt, map_locationcpu) img cv2.imread(test.jpg) results detector(img, size640) boxes results.xyxy[0].numpy() # [x1, y1, x2, y2, conf, cls] for box in boxes: x1, y1, x2, y2 box[:4].astype(int) roi img[y1:y2, x1:x2] # 裁剪车牌区域 roi preprocess_plate(roi) # 等比缩放 padding 到 32x140 plate_chars recognizer(roi) # LPRNet forward CTC 解码 print(f识别的车牌: {plate_chars})这里的 preprocess_plate 是识别模型的固定预处理先把高度 resize 到 32宽度按比例缩放剩余部分像素值填充到 140。打印前记得 nms 和置信度阈值过滤我一般用 conf0.5低于这个阈值说明检测框本身不可信识别结果没必要继续算。框架版本不同 attempt_load 的导入路径有差异如果找不到这个函数改成 torch.hub.load 加载本地权重也行。6.2 验收指标mAP、字符准确率、单张耗时怎么测答辩或者自测时不要只看几张图的效果。检测部分跑官方验证命令python val.py \ --data data/plate.yaml \ --weights weights/plate_detect_best.pt \ --img 640输出里重点关注mAP0.5和mAP0.5:0.95。对车牌这种单类任务0.5 的 IoU 阈值就能反映框的定位质量0.95 那个指标受小目标影响大容易偏低不用太纠结。识别部分单独统计字符准确率把所有测试车牌图块依次过识别模型逐字符对比预测和标注算准确的字符数除以总字符数。这里有个习惯问题别把“整牌正确率”和“字符正确率”混着报答辩老师要是追问能说清楚两者区别很加分。6.3 导出演示ONNX 格式和现场检查单演示环境如果不是训练机建议把两个模型都导出成 ONNXCPU 上也能跑python export.py \ --weights weights/plate_detect_best.pt \ --include onnx --img 640导出后检查三个点输入尺寸是不是 640输出是不是检测框格式类别置信度是不是只有一类。识别模型同理导出后用同一张图对比 ONNX 和 PyTorch 的输出是否一致再部署。我自己的习惯是准备一份检查单测试图包含蓝牌、绿牌、夜间、倾斜四类样本每类 5 张以上先跑检测定位再跑识别最后记一张总耗时。从那以后我每次做检测加识别的工程都会强制走一遍数据格式、字符集核对、anchors 重算和 ONNX 一致性检查这几步固化成习惯了。希望帮到你。本文还有配套的精品资源点击获取