简介在智能交通与停车管理场景中车牌识别系统通常由目标检测与字符识别两部分组成。目标检测技术用于在复杂背景中定位车牌区域而序列识别算法则负责将车牌图像转换为字符文本。YOLOv5作为单阶段检测器的代表具备高速度与多尺度检测能力LPRNet则是一种轻量级端到端识别网络通过CTC损失函数实现无需字符分割的序列预测。两者结合可构建高性价比车牌识别流水线。CCPD数据集作为真实停车场场景的大规模数据源其独特的文件名编码方式为训练提供了丰富的标注信息。本文围绕CCPD数据处理、YOLOv5检测模型训练、LPRNet识别模型调优及系统集成的完整链路分享实际工程中的踩坑经验与优化思路助力开发者快速搭建可落地的车牌识别方案。 车牌识别这个方向我前前后后折腾了大概两个月从最开始只会调YOLOv5现成权重到后面把LPRNet整个训练推理链路跑通再到把检测和识别串成一条完整流水线中间踩的坑比想象中多得多。如果你正在研究YOLOv5车牌检测和LPRNet车牌识别并且打算用CCPD数据集来搞一套自己的方案那这篇东西应该能帮你省下不少时间。我会尽量把整个项目的思路、关键细节、实操步骤和排坑经验一次讲清楚不讲废话专注让你能把项目真正跑起来。1. 项目整体设计与思路拆解1.1 车牌识别系统要解决的核心问题车牌识别本质上是两个任务的串联先得在图像里找到车牌在哪里再把找到的车牌区域里的字符读出来。听起来简单实际做起来牵扯的问题不少。首先是检测环节车牌在画面里可能有大小差异、角度倾斜、光照不均、遮挡模糊等各种情况检测模型必须在这种复杂背景下稳定地框出车牌位置。其次是识别环节拿到检测框之后里面的字符排列并不一定是水平的有可能是倾斜的、模糊的甚至带着铆钉和边框干扰识别模型要从这些干扰里把汉字、字母、数字逐个认出来。这套组合里YOLOv5负责检测LPRNet负责识别两者的分工非常明确。YOLOv5是典型的单阶段目标检测器速度和精度平衡得比较好在车牌这种相对规则的检测目标上表现很稳。LPRNet则是一个不需要字符级分割的轻量级识别网络它直接对整张车牌图像做序列识别通过CTC损失来对齐字符序列这样省去了传统方案里先切字符再逐个识别的麻烦也让整个流程简洁了不少。1.2 为什么选YOLOv5LPRNet这套组合说句实话车牌识别不是没有更省事的方案比如直接用HyperLPR或者PaddleOCR里现成的车牌识别模型但自己搭一套YOLOv5LPRNet的组合有一个明显的好处可定制性极强。你可以随时替换训练数据调整检测策略甚至把LPRNet的backbone换掉来适配边缘设备。对于实际工程项目来说这种可控感很重要。YOLOv5在检测端的优势不用多说生态成熟、部署方案多、训练资料全。它针对小目标和密集场景的优化比较到位车牌在监控画面里往往只占很小一块区域YOLOv5的多尺度检测能力可以覆盖从近到远的各种车牌尺寸。LPRNet这边的选型理由也很实在它的参数量只有不到两百万在CPU上也能跑到几十毫秒级别的推理速度这在很多需要部署到低成本设备的场景里特别吃香。同时LPRNet用的是全局特征序列识别不需要精确的字符切分对车牌字符粘连、边框干扰的容错能力更强。1.3 整体流程架构我的整体架构分四步走第一步把CCPD数据集里的图片和标注解析出来第二步训练YOLOv5检测模型得到能框出车牌区域的权重第三步训练LPRNet识别模型把裁剪出来的车牌图像转成字符串第四步把检测和识别串成一个完整的推理流水线输入一张大图直接输出车牌号。考虑到CCPD数据集的标注格式比较特殊它把车牌的四个角点坐标直接编码在文件名里所以第一步的数据解析是整条链路里最容易出问题的地方。后面我会专门花一节讲这块因为数据没处理好后面训练什么都是白搭。2. 环境准备与数据集深度解析2.1 环境配置要点先明确一下我的环境版本帮助你做个参考。我用的是Ubuntu 20.04Python 3.8PyTorch 1.10.0CUDA 11.3显卡是RTX 3060 12G。YOLOv5用的官方6.0版本LPRNet用的开源PyTorch实现。装环境这件事看似常规但有三个细节值得注意。第一PyTorch和CUDA的版本一定要匹配建议直接去PyTorch官网用对应命令安装不要自己猜版本组合。第二如果训练数据量比较大建议把torch、torchvision、opencv-python这些包的版本锁定避免后面莫名奇妙的兼容性问题。第三LPRNet那个开源实现里用到的warp_ctc_pytorch在有些环境里编译比较麻烦我试下来直接用原生PyTorch的CTCLoss替代完全没问题这也省去了自己编译扩展库的麻烦。提示训练LPRNet前可以先把项目里的依赖装齐我用的是requirements.txt一次性解决避免反复装包浪费时间。2.2 CCPD数据集结构拆解CCPD数据集全称是Chinese City Parking Dataset是车牌识别领域用得比较多的数据集总共大概30万张图片全部来自真实停车场场景。它的标注方式非常有意思所有关键信息都直接编码在文件名里不需要额外的JSON或XML标注文件。具体看一个文件名例子025-95_113-154383_386473-386473_177454_154383_363402-0_0_22_27_27_33_16-37-15.jpg这个文件名里每个字段用横杠分隔分别代表字段含义025车牌省份缩写对应的编号95_113车牌区域的大致中心坐标和宽高信息154383_386473车牌左上角坐标154,383和右下角坐标386,473386473_177454_154383_363402车牌的四个角点坐标顺序依次是右下、左下、左上、右上0_0_22_27_27_33_16亮度、模糊度等属性信息37车牌类型对应的编号15车牌颜色类型这里面最关键的字段就是四个角点坐标。为什么需要角点坐标而不是直接给一个矩形框因为CCPD里的车牌很多是倾斜的简单的水平矩形框没办法准确框住倾斜车牌用角点坐标方便后面做透视变换校正。2.3 数据预处理与标注格式转换YOLOv5训练需要的是归一化的中心坐标加宽高格式标注也就是每张图片对应一个txt文件每行是class_id x_center y_center width height。CCPD给的是角点坐标所以我们要自己转。我的转换逻辑很简单取四个角点中x和y的最小值和最大值构成一个包含车牌的外接矩形然后归一化写入txt。这种做法虽然会引入一点背景噪声因为外接矩形比实际车牌区域略大但实际操作下来对检测精度影响很小。CCPD官方没有给训练集和验证集的划分文件我是自己按比例划分的。我选了大概10万张图片做训练1万张做验证随机打乱后按文件名索引存成train.txt和val.txt。注意划分的时候要保证车牌省份的分布尽量均匀不然某个省份的字符可能训练不到。LPRNet那边不需要标注文件它直接从图片文件名里读取车牌字符串。CCPD文件名里第一个字段代表省份缩写但完整的车牌字符序列其实要看文件名里的一个映射关系。实际用起来比较麻烦因为CCPD的命名规则没法直接反推出车牌号码。我的处理方式是把检测框裁出来之后手动标注了大概5万张车牌图片按省份、字母、数字的标准格式做标签这在后面LPRNet训练里直接用。注意CCPD文件名里的第一个字段是省份缩写编号需要自己维护一个字典映射比如0对应皖、1对应沪、2对应津、3对应渝、4对应冀等具体映射表在CCPD相关论文和开源代码里都能找到。3. YOLOv5车牌检测模块实现3.1 YOLOv5模型结构与检测原理YOLOv5的模型结构由Backbone、Neck和Head三部分组成。Backbone使用CSPDarknet负责提取图像特征Neck使用PANet结构把不同层级的特征图融合起来这样既能检测大目标也能检测小目标Head负责输出最终的边界框和类别概率。车牌检测和通用目标检测有个不太一样的地方车牌的宽高比非常固定大概在3:1到4:1左右而且字符集中在一个矩形区域内。这意味着YOLOv5的anchor尺寸其实可以针对车牌做特殊调整。官方默认的anchor是80类COCO数据集聚类出来的用在车牌上不算最优解。我训练前对数据集重新做了K-Means聚类算出了适合车牌尺寸的anchor例如[8,4, 12,5, 18,8]、[28,12, 40,16, 58,22]、[72,28, 96,36, 128,48]这样的组合。实测下来收敛更快mAP也有小幅提升。3.2 训练参数配置与调优训练YOLOv5的关键参数有这几个img-size、batch-size、epochs、学习率。我这边因为GPU显存有限把img-size设为640batch-size设为32epochs设了100轮。学习率用的是YOLOv5默认的余弦退火策略初始学习率0.01最终学习率0.001。有几个训练细节想特别提醒一下。第一YOLOv5默认会做Mosaic数据增强把四张图拼成一张训练这对小目标检测帮助很大但是到了训练后期建议把Mosaic关掉因为过度的拼接可能让模型对小目标的定位不够精细。我在最后20个epoch把mosaic关闭了验证集上的mAP确实涨了一点。第二车牌检测里类别只有一个也就是plate这一类如果误设成多个类别会带来没必要的计算开销。第三训练过程中要留意验证集的mAP0.5和mAP0.5:0.95这两个指标如果mAP0.5已经到98以上但mAP0.5:0.95偏低说明模型对精确定位还不够好可以适当增加epoch或者调小anchor的缩放比例。训练命令大概是这样的python train.py --data plate.yaml --weights yolov5s.pt --img 640 --batch-size 32 --epochs 100 --device 0其中plate.yaml的内容大致如下train: /data/ccpd/train.txt val: /data/ccpd/val.txt nc: 1 names: [plate]3.3 模型推理与检测结果处理训练完成后推理就简单了。加载best.pt权重输入图片输出带置信度的检测框。这里有个实操细节检测置信度阈值和NMS的IoU阈值需要配合着调。我这边置信度阈值设0.5NMS的IoU阈值设0.45效果比较平衡。如果阈值设太高容易漏检模糊车牌设太低容易出现重复框或者误检。测试下来置信度阈值0.4到0.5之间比较适合停车场场景。因为车牌是一个矩形目标我在推理后还会做一个过滤操作把检测框的宽高比过滤在2.5到5.0之间低于或者高于这个范围的基本都是误检直接丢弃。这个简单的小技巧能显著减少误检率尤其在画面里有反光、招牌、纹理等干扰的情况下特别有用。实操心得检测框输出之后不要急着把图片裁出来做识别建议先根据角点信息做透视校正。YOLOv5输出的是水平外接矩形但车牌本身可能是倾斜的水平裁剪会把背景和相邻车牌的干扰带进来影响LPRNet识别效果。4. LPRNet车牌识别模块实现4.1 LPRNet网络结构与识别原理LPRNet的核心思路是端到端的序列识别不需要先把车牌的每个字符切出来。它接收一张固定高度的车牌图片经过CNN提取特征序列再通过RNN建模序列上下文关系最终用CTC损失函数完成序列到序列的对齐。你只需要告诉网络这串字符是什么它自己会学到字符之间的位置对应关系。LPRNet的backbone其实不复杂由若干个卷积层和降采样层组成最后接一个双向LSTM来增强序列特征。整体参数量大概在1.5M左右对比动辄几十M的大模型这个体量对边缘设备非常友好。这也是我选它做识别端的核心原因后续如果要部署到Jetson Nano或者树莓派上性能至少是能接受的。4.2 字符集设计与标签处理车牌识别的字符集相比OCR通用场景要小得多只包含省份汉字、字母和数字。我国车牌第一位是省份汉字共有31个省级行政区简称第二位是发牌机关代号字母后面是字母和数字的组合。我用的字符集有68个字符包括31个汉字、24个字母I和O不用于普通车牌、10个数字再加上一个空白符用于CTC对齐。标签处理这里有个容易踩坑的地方。CCPD数据集的标注方式不直接给出车牌字符串需要从文件名解析或者手动标注。我自己做了一个辅助工具用YOLOv5检测出车牌区域后截图保存再用一个简单的UI工具批量打标签最终得到一份格式为图片路径 车牌字符串的标签文件。这部分工作量确实不小但数据质量决定模型上限值得花时间做。4.3 模型训练与关键参数LPRNet训练时输入图片的高度我固定为48宽度自动缩放保持和原始车牌比例一致。训练过程中的核心参数包括参数数值说明输入高度48网络要求固定高度批量大小64根据显存调整初始学习率0.01使用Adam优化器学习率衰减每5轮衰减0.1逐步收敛训练轮数60足够达到收敛损失函数CTC Loss原生PyTorch实现训练过程中需要关注两个指标一个是loss值一个是字符准确率。我这边大概训练到第20轮的时候loss已经降得比较慢但字符准确率还在缓慢上升所以还是让它跑满60轮。LPRNet训练对数据集的要求不高5万张图片已经足够训练出一个识别准确率很高的模型了。4.4 车牌图像预处理与校正把检测框里的车牌喂给LPRNet之前一定要做图像校正。前面提到YOLOv5输出的是水平外接矩形如果直接裁剪得到的是包含车牌但可能有倾斜的矩形图。我用的方法是用OpenCV做透视变换把四个角点映射到一个标准的水平矩形上。具体来说用cv2.getPerspectiveTransform算一下变换矩阵再用cv2.warpPerspective把区域扳平。这一步对识别准确率的提升非常明显尤其在CCPD数据集里倾斜车牌占比不低的情况下。预处理还有一个细节是归一化。LPRNet训练时用的是ImageNet的均值和方差做标准化推理时也要保持一致不然特征的分布会偏移识别成绩会打折。5. 检测与识别系统集成5.1 完整推理流水线搭建检测和识别单独跑通之后下一步就是把它们串起来。我的流水线设计是读入原始图像交给YOLOv5得到检测框对每个检测框做透视校正和预处理再交给LPRNet得到车牌字符串最后把结果标注回原图。流水线的核心代码如下import cv2 import torch import numpy as np def detect_recognize(frame, detect_model, recognize_model): # 1. YOLOv5检测 results detect_model(frame, size640) boxes results.xyxy[0][:, :4].cpu().numpy() scores results.xyxy[0][:, 4].cpu().numpy() plates [] for box, score in zip(boxes, scores): if score 0.5: continue x1, y1, x2, y2 [int(v) for v in box] # 宽高比过滤 w, h x2 - x1, y2 - y1 if w / h 2.5 or w / h 5.0: continue # 2. 裁剪车牌区域 plate_img frame[y1:y2, x1:x2] # 3. LPRNet识别 plate_text recognize_model(plate_img) plates.append((box, score, plate_text)) return plates实际跑起来检测加识别整体耗时在GPU上大约30毫秒CPU上大约150到200毫秒可以满足绝大多数停车场道闸场景的实时性要求。5.2 性能优化手段如果觉得速度还不够有几个方向可以优化。第一个方向是模型轻量化YOLOv5从s到n有很多版本我在实际部署时选了YOLOv5n精度掉得不多但速度几乎翻倍。第二个方向是TensorRT加速把YOLOv5和LPRNet都转成TensorRT的engine文件推理速度还能再快不少。第三个方向是检测帧率控制停车场场景里车牌变化并不频繁没必要对每一帧都做完整检测识别可以做帧差判断画面没变化时直接复用上一次的结果这个优化效果立竿见影。注意转TensorRT时如果遇到算子不兼容的问题建议先看看YOLOv5和LPRNet里有没有用到动态shape如果有需要先把输入尺寸固定成静态shape再导出能省去很多麻烦。6. 常见问题与排查经验6.1 数据集解析时遇到文件名编码问题CCPD的文件名包含中文字段吗不包含但它用数字编码表示省份汉字。有些新手朋友在使用正则解析文件名时分不清154383_386473这种坐标格式容易解析错位置。我的建议是写一个专门的解析函数先按横杠分段再逐字段解析每一步都print出来核对不要一股脑写完整流程再调试。6.2 车牌检测框不准怎么办如果YOLOv5预测的检测框和真实车牌贴合不够紧密框偏大或者偏小可以在后处理里做一个外扩或者收缩。我实际测试发现检测框略微往外扩几个像素对LPRNet识别的准确率反而有帮助因为太紧的框可能裁掉车牌边缘的字符。但扩太多也不行会把背景噪声引进来。我这边用的是宽度和高度各扩5%的比例。6.3 LPRNet识别结果乱码问题识别结果乱码有几种可能。第一种是字符集没有对齐训练时字符集的索引顺序和推理时不一致这种问题一般是代码bug仔细检查映射关系就行。第二种情况是车牌图像预处理和后处理不匹配比如训练时用了灰度图推理时却传了彩色图。第三种情况是车牌倾斜太严重透视校正做得不够好可以增加一些随机旋转和透视变换的数据增强来提升模型的鲁棒性。6.4 GPU显存不足的解决方案训练YOLOv5时如果GPU显存不够有几个常用的办法。降低batch-size是最直接的思路但会影响BatchNorm的统计效果建议同步调小img-size或者改用更小的模型版本。还可以开启梯度累积等效扩大batch-size。LPRNet那边显存占用很小一般不用担心。6.5 从CPU到GPU迁移推理的坑开发阶段可能在CPU上跑通代码部署时换到GPU上发现结果不一致。这种问题多半是模型权重加载时device没有正确指定或者输入张量没有转移到GPU上。排查思路很简单把模型的device和输入的device分别打印出来对比一下能解决大多数问题。6.6 夜间和强光场景的效果提升如果车牌识别要在夜间或者强逆光场景下工作单纯靠模型很难做到完美。我试过两个有效的手段一个是加图像增强预处理比如CLAHE自适应直方图均衡化提升暗部细节另一个是用视频流的多帧融合策略连续取几帧做识别取置信度最高的结果能显著降低单帧光照干扰导致的误识别。7. 后续可扩展的方向这套YOLOv5LPRNet的方案跑通之后可以往好几个方向继续扩展。比如检测端可以换YOLOv8或者RT-DETR精度和速度都有提升空间识别端可以尝试加入注意力机制增强对模糊车牌的建模能力部署端可以往TensorRT、ONNX Runtime、RKNN等方向走适配不同的硬件平台。还可以考虑从单张图片识别扩展到视频流识别加入目标跟踪模块实现多帧跟踪识别提高识别置信度和稳定性。对停车场管理系统来说结合车辆颜色识别、车标识别等功能能构建更完整的车辆结构化信息输出。我个人经验是这类项目最重要的是把数据处理管线和模型训练流程彻底跑通让每一个环节都变成可复用的工具链。这样一来无论以后换数据集、换模型还是换部署平台都只是局部模块的替换整体框架不需要重写。这个思路分享一下希望能帮你在自己的车牌识别项目里少走一些弯路。本文还有配套的精品资源点击获取