基于yolov5与LPRNet的车牌识别系统实战与部署优化
简介目标检测与光学字符识别是计算机视觉中两项基础技术广泛应用于智能交通、安防监控等场景。车牌识别系统是二者的典型结合——通过目标检测模型定位车辆牌照区域再借助序列识别模型将字符图片转换为文本。本文从工程实践角度出发梳理了一套基于yolov5的车牌检测、颜色分类与LPRNet车牌号识别的完整方案覆盖数据标注、模型训练、透视矫正、ONNX导出与TensorRT加速等关键环节并针对夜间反光、颜色混淆、字符相似等真实场景中的常见问题给出了可行的兜底策略。通过合理的模型拆分与部署优化系统在识别精度和实时性能之间取得良好平衡可为停车场管理、卡口监控等实际业务提供可靠支撑。 做车牌识别这个方向前后折腾了快一年时间。最开始用的传统图像处理方案颜色阈值分割加轮廓查找在固定角度、固定光线的停车场demo上能跑到95%一换到真实路面就崩得没法看。后来切到yolov5做检测再把车牌号识别独立出来整体效果才真正稳下来。这套系统目前包含三个能力车牌位置检测、车牌颜色识别、车牌号识别。本文就把整个踩坑过程和最终落地的方案完整写出来给准备做类似项目的朋友一个参照。1. 为什么做车牌识别先选了yolov5而不是传统视觉方案1.1 车牌识别系统的完整工作链路拆解一个完整的车牌识别系统表面上看是输入一张图输出一串车牌号但内部其实是一条流水线检测、矫正、分类、识别。每个环节的误差都会传导到最终结果所以不能指望一个模型解决所有事。具体来说输入图片后第一步是定位车牌区域这一步决定后面所有工作的输入质量。第二步是拿到检测框后对框内的车牌图像做透视矫正把倾斜、畸变的车牌拉正。第三步是判断车牌颜色蓝牌、黄牌、绿牌、白牌、黑牌的归属这在停车场计费和新能源车识别场景里是硬需求。第四步才是把矫正后的字符区域转成文本也就是车牌号识别。早期我用OpenCV做第一步通过边缘检测、形态学操作、轮廓筛选来找车牌位置。在单一场景下效果尚可但光照变化大、背景复杂、车辆运动模糊时召回率直线下降。换到yolov5之后检测环节变成了一个目标检测模型的问题泛化能力由一个数据集来保证而不是靠人工调阈值。这是我决定转向深度学习方案的直接原因。1.2 yolov5在检测-分类-识别三层任务中的定位yolov5在这一整套系统里承担的是第一层任务也就是定位车牌位置。它输出的不是车牌号而是一个或若干个矩形框以及每个框对应的类别和置信度。注意这里有个容易混淆的点yolov5也被很多人用来做端到端识别比如直接把整张车牌图训练成每个字符一个检测框的字符检测模型。这种做法并不是不行但工程上我更推荐拆开yolov5只做车牌区域检测车牌号识别交给CRNN或者LPRNet这类序列识别模型。原因有两点。第一目标检测模型输出的是框和类别它不擅长表达字符顺序。国内车牌字符数量固定但也有新能源车牌的差异如果检测出字符框再排序拼接会多一道排序逻辑而且每个字符单独一个框误检率会叠加。第二序列识别模型天然处理变长文本直接输出省份简称字母数字的序列端到端训练省去中间所有人工规则。所以yolov5在系统里的角色非常纯粹负责找车牌的位置和颜色把内容交给OCR模块。1.3 版本选型yolov5s与yolov5m的取舍yolov5官方提供了n/s/m/l/x五个规模主要区别在骨干网络的深度和宽度。我实际测试下来在车牌检测这个相对简单的单类目标任务里yolov5s是性价比最高的选择。用COCO预训练权重作为起点在自建车牌数据集上微调yolov5s在GPU上的推理速度大约3到5毫秒每帧取决于输入分辨率mAP50能达到98%以上。yolov5m的精度提升大约0.5到1个百分点但推理时间几乎翻倍在边缘设备上差距更明显。如果你的部署环境是Jetson Nano或者RK3588这类算力有限的设备yolov5s甚至yolov5n是更稳妥的选择。当然如果你用的是高分辨率输入比如1600像素以上的原图且场景里有大量远距离小目标车牌可以尝试yolov5m。车牌检测的难点不在类别多而在目标尺度变化大所以输入分辨率对效果的影响比模型本身更大。2. 车牌检测模型标注、训练与收敛判断2.1 数据集怎么搞CCPD之外还得靠自己补车牌检测数据集最常用的是中科大的CCPDChinese City Parking Dataset包含超过25万张真实场景的车辆图片按场景分为CCPD-Base、CCPD-Weather、CCPD-Nature等子集每张图标注了车牌四个角点坐标。直接用CCPD训练yolov5检测精度就能做到很高但它不是没有坑。CCPD的图片是从停车场监控视角拍摄的场景相对单一视角偏高背景多为停车场内部。如果你要识别的场景是路面卡口、高速出入口、或者路侧停车直接用CCPD训练的模型会存在一定的域偏移表现就是漏检率上升。我的做法是以CCPD-Base为主加上自己拍摄标注的路面场景图片大概2000到3000张混合训练之后在自测集上的漏检率下降非常明显。自己标注车牌时需要注意标注框的紧致度。车牌检测框不要松松垮垮包住整个车头也不要只框住字符区域。我通常的做法是框住整个车牌含边框四个角尽量贴边这样yolov5学到的特征会集中在车牌边框和底色上对后续的颜色识别也有帮助。2.2 标注规范和yolov5超参数调优实践yolov5的标注格式是YOLO格式即归一化的中心点坐标和宽高。如果你用LabelImg标注注意导出格式要选YOLO格式而不是VOC XML。CCPD自带的是四个角点的坐标需要写个小脚本转换成YOLO格式网上有现成的转换工具但建议自己写一遍顺便可以做数据清洗。数据清洗这一步容易被忽略。CCPD里有些图片车牌区域严重遮挡或者模糊到人眼都看不清这类图片在训练时要过滤掉否则会干扰模型收敛。我保留的原则是车牌在图片中可见面积大于整体面积的三分之一且人眼能分辨至少三个字符。训练yolov5时超参数里面影响最大的是img_size、batch_size和epochs。我的起点配置是img_size640兼顾速度和精度车牌检测不需要太高分辨率。batch_size根据显存来8G显存配16到32。epochs设置在150到200之间配上早停机制。yolov5的训练命令很简单但要注意指定预训练权重。用--weights yolov5s.pt --data data.yaml --img 640 --epochs 150 --batch 16。data.yaml里如果只有一类车牌nc设为1类别名写plate。yolov5的默认超参数对车牌检测已经比较友好不建议一开始就动lr和mosaic这类参数。唯一值得改的是mosaic增强概率如果你训练数据里有大量图片中的车牌本身就比较小可以适当提高mosaic因为这种增强方式模拟了多个目标在同一图中的场景有助于提升小目标检测能力。我一般把mosaic设为1.0训练后期再关闭。2.3 训练过程怎么判断模型真的练好了判断yolov5训练是否到位不能只看最终的mAP还要看训练过程中的三条曲线val/box_loss、val/obj_loss、metrics/mAP_0.5。一个正常的训练过程是box_loss和obj_loss在前30个epoch快速下降之后进入平台期mAP稳步上升后趋于平稳。如果box_loss降得慢或者出现震荡很大概率是数据问题比如标注框不准确或者类别不平衡。如果mAP一直很低先看是不是数据集的类别标签写错了再考虑数据量。我自己习惯用预热训练的方式先用--epochs 50快速跑一遍确认loss有下降趋势再从头开始完整训练。这样能在前期快速发现数据问题避免完整训练几小时后才发现标注有误。训练完成后用test.py或者val.py评估模型重点看三个指标precision、recall和mAP50-95。车牌检测场景里召回率比精确率更重要漏掉一块车牌整个系统就失效了而误检可以由颜色识别和OCR模块二次过滤掉一部分。所以如果你的precision能到98%但recall只有95%我建议继续补充数据或者调整置信度阈值目标是把recall拉到98%以上。3. 车牌颜色识别不止是蓝绿黄三个标签3.1 用同一模型输出颜色类别还是单独做分类器颜色识别这块有两条技术路线。一条是把颜色作为yolov5的类别比如训练一个多类别检测模型类别包括blue_plate、green_plate、yellow_plate等。另一条是检测用单独模型颜色识别用轻量分类网络或者颜色直方图规则。两条路我都试过最终选择了后者。为什么因为把颜色和位置绑在同一个yolov5模型里意味着训练数据需要覆盖同一位置、不同颜色的组合一旦某个颜色类别的样本不足模型很容易把颜色和位置特征耦合起来。比如训练集里蓝牌大多出现在车头正面绿牌大多出现在车尾模型就会倾向于看到车头就预测蓝牌。这是一个很隐蔽的坑。单独做颜色识别输入是yolov5检测出的车牌区域输出是颜色类别。这个分类task的输入明确数据构造也容易只需要把原始标注数据按车牌颜色分类即可。我用的分类网络是ResNet18输入尺寸64x64RGB三通道全连接层输出4类blue、green、yellow、white。训练50个epoch就能达到99%以上的准确率。3.2 颜色类别定义与样本均衡问题颜色类别定义要看业务场景。常见的有蓝牌燃油车、绿牌新能源注意绿牌分渐变绿和黄绿双拼、黄牌大型车、白牌警车、军车、黑牌涉外车辆。如果只是做民用停车场系统蓝绿黄三类基本够用。样本不均衡是颜色分类最大的问题。蓝牌在总数里占比可能超过80%绿牌大约15%黄牌不到5%。如果不做处理模型会倾向于把不确定的样本判成蓝牌。我用了三种方法缓解过采样少数类对黄牌和绿牌图片做随机旋转、亮度抖动、裁剪把样本量均衡到蓝牌的一半左右。用WeightedRandomSampler按类别权重采样替代普通的DataLoader采样。训练时用Focal Loss替换CrossEntropyLoss让模型更关注难分类样本。还有一个细节是颜色分类的输入不能直接取yolov5检测框的原始裁剪图因为光照、白平衡会显著改变颜色表现。我做的预处理是三段式先灰度化再做直方图均衡化增强对比度最后通过色相统计来辅助判断。后面细说。3.3 白天晚上色差那么大怎么保证颜色模块稳定性颜色对光照极其敏感同一块蓝牌白天是亮蓝晚上在黄光灯下可能偏灰蓝或墨绿。想让模型稳定单纯靠训练数据覆盖不够我加了一套兜底规则。我的方案是模型预测为主HSV色相统计为辅。模型输出颜色置信度如果最高置信度大于0.85直接采纳。如果置信度较低比如几个颜色类别概率都在0.2到0.4之间就用传统HSV方法兜底把车牌区域的BGR图转换到HSV空间统计H通道的分布蓝色色相在100到130绿色在35到77黄色在15到35。根据色相分布占比来判定颜色。这套混合策略在夜间和隧道场景里明显降低了蓝绿混淆的发生率。另外一个实用技巧是颜色识别前先判断车牌区域的平均亮度如果平均亮度过低先做一次自适应直方图均衡化CLAHE再送分类器效果会好很多。4. 车牌号识别搞定透视矫正和字符序列输出4.1 检测框到OCR输入的透视矫正yolov5输出的是带角度的矩形框如果车牌在画面中有倾斜或透视形变直接把这个区域送进OCR模块识别率会大打折扣。所以在车牌号识别前必须做透视矫正。yolov5标准输出是x1, y1, x2, y2这种轴对齐坐标但如果检测框是倾斜的这种表示本身就是不准确的。我在检测时用的是yolov5的--save-txt的Detect输出再结合对车牌四角点的直接回归。具体做法是在yolov5检测出来后用额外的角点回归头预测车牌的四个角点坐标然后用OpenCV的getPerspectiveTransform计算变换矩阵把车牌区域矫正成宽240像素、高80像素的矩形。如果不想改yolov5结构也可以用轮廓近似的方式从检测框内找出车牌的四个角点。因为车牌本身有高对比度的边框在检测框内再做一次Canny边缘检测和轮廓查找找到面积最大的四边形轮廓近似出四角点。这个方案在多数场景下够用但遇到车身边缘干扰时会失效鲁棒性不如直接回归角点。矫正之后还有一个细节车牌字符区域实际上只占车牌中间大约90%的区域上下各有一段边框。直接送OCR之前最好按比例裁掉上下边框比如去掉上下各5%的像素这样能减少底色的干扰。我实测这个裁剪能让OCR准确率提升1到2个百分点。4.2 LPRNet端到端识别为什么我不用字符分割国内外车牌字符识别主流方案有两种字符分割单字符分类以及端到端序列识别。字符分割的思路是先把车牌图片按字符间隙切分成单个字符然后用分类模型逐个识别。这种方案在标准车牌上表现不错但一旦遇到字符粘连、铆钉干扰、边框噪声分割就出错后续所有字符跟着错。我放弃字符分割的另一个重要原因是国内新能源车牌比传统蓝牌多一位且字符排列更紧凑。如果分割器是按字符位置切分的更换车牌类型就等于重新调算法。所以我直接选了LPRNet这种轻量级端到端识别网络。LPRNet的结构是CNN主干加RNN序列建模最后接CTC Loss。它的输入是矫正后的车牌图片输出是字符序列的概率分布。训练时不需要对每个字符的位置做标注只需要提供车牌号字符串这对数据准备非常友好。LPRNet的PyTorch实现网上很多我用的版本是改过的轻量结构参数量大约5M在GPU上单帧推理不到2毫秒在CPU上也有20毫秒左右的速度。作为对比我试过用CRNN加ResNet18作为backbone识别效果接近但模型体积和推理时间都更大。车牌字符数量有限、类别固定LPRNet这种小模型完全够用。4.3 字符集定义和识别结果后处理车牌识别的字符集和通用OCR不太一样需要单独定义。国内车牌字符包括省份简称京、津、冀、晋、蒙、辽、吉、黑、沪、苏、浙、皖、闽、赣、鲁、豫、鄂、湘、粤、桂、琼、渝、川、贵、云、藏、陕、甘、青、宁、新等。字母A-ZI和O在民用号牌中通常不用但军警等特殊车牌可能出现所以保留。数字0-9但0和O、1和I容易混淆识别后处理要做规则替换。LPRNet输出的是每个时间步的字符概率解码用CTC贪心搜索或者beam search。贪心搜索速度快在车牌这个固定长度场景下和beam search差距不大所以我用的贪心。解码后得到一串字符再根据业务规则校验普通蓝牌是省份简称字母5位数字/字母新能源绿牌是省份简称字母6位数字/字母。后处理是我认为整个识别流程里最值得下功夫的地方。我写了三条规则首字符必须是省份简称字典里的汉字否则判定识别失败或修正为候选集里概率最高的省份字。第二位必须是字母如果模型输出数字按混淆矩阵把它修正为最接近的字母。字符长度校验蓝牌7位绿牌8位长度不对时对置信度最低的字符做二次分类或者直接丢弃该帧让视频流里的下一帧重新识别。这些规则听着简单但在实际场景里能把最终准确率提升3到5个百分点。比如0和O的混淆如果后验地看上下文第二位不可能是数字那模型输出0就一定是O直接替换。5. 模型部署与实时性优化从PyTorch到端侧推理5.1 ONNX导出和TensorRT加速的实操记录模型在PyTorch里训练好后不能直接用于生产环境我用ONNX做中间格式再转到TensorRT做GPU加速。yolov5官方仓库自带export.py一条命令就能导出ONNXpython export.py --weights best.pt --include onnx --img 640 --dynamic。这里要小心--dynamic选项动态batch和动态输入尺寸会降低TensorRT的优化效果。如果业务场景输入尺寸固定建议导出固定尺寸ONNX。LPRNet导ONNX稍微麻烦一点因为它涉及CTC解码里面的torch.argmax和去重操作在导出时可能报错。我的处理方式是只导出CNN主干部分把CTC解码逻辑放到推理代码里用CPU执行。这样既保留了模型精度又避开了ONNX对动态循环的不友好。TensorRT加速时我用trtexec把ONNX转成engine文件关键参数是--fp16和--maxBatch。车牌检测和OCR模型都是小模型FP16精度损失几乎可以忽略但推理速度能提升2到3倍。如果部署在Jetson系列设备上记得用设备自带的TensorRT版本不同的JetPack版本对应不同TensorRT版本直接用宿主机上的trtexec容易踩ABI不兼容的坑。引擎文件是跟GPU架构绑定的换一台机器就得重新生成。这一步我吃过亏把Jetson上生成的engine直接拷到另一台同型号设备上跑结果加载失败。后来才发现TensorRT的engine和CUDA版本、TensorRT版本甚至GPU的SM架构都强相关。这里也提醒一下生产环境的镜像和驱动版本一定要提前锁死不然每次部署都要重新折腾编译链路。5.2 预处理/后处理耗时才是真正的大头做实时系统优化时很多人会紧盯模型推理时间把yolov5从5毫秒优化到3毫秒就觉得大功告成。但实际端到端延迟里预处理和后处理往往占掉一半时间。我的系统输入是IPC摄像头流先用FFmpeg拉流解码成BGR图。解码一帧1080p视频耗时大约10到15毫秒如果再加上图像缩放、归一化、颜色空间转换又是5到8毫秒。这还没算yolov5的NMS后处理NMS在原图上跑框多的时候能到3到5毫秒。所以整个链路下来模型推理只占三分之一的时间。针对这个瓶颈我做了几个优化输入尺寸从640降到416车牌本身是大目标416精度损失很小但推理和预处理都明显变快。NMS换成Fast NMS或者把conf_thres从0.25提升到0.45减少进入NMS的候选框数量。车牌检测这类单类目标候选框数量本身少这个优化效果没那么夸张但还是有收益。预处理用CUDA上的cudapreprocess或OpenCV的UMat异步上传GPU避免CPU到GPU的拷贝阻塞管线。视频解码用硬件解码FFmpeg开启h264_cuvid或者d3d11vaCPU占用率能降到原来的三分之一。后处理中还有一个容易被忽略的环节是颜色识别和OCR的输入裁剪。如果每帧都要从原图里截取车牌区域再做缩放频繁的cv2.resize会引入额外延迟。我改成从检测框直接计算需要的缩放比例一次warpAffine完成裁剪和矫正减少一次resize。5.3 多路摄像头场景下的线程与显存设计真实项目里很少是单路摄像头大多是8路、16路甚至更多并发推理的设计就变得很关键。我踩过的最大坑是照搬单路推理代码去跑多路结果显存爆掉或者延迟忽高忽低。我的方案是生产消费者模式加共享推理线程池。每个摄像头管线是一个生产线程只负责拉流、解码、预处理然后把处理好的tensor放进队列。一个独立的推理线程池从队列取tensor执行yolov5检测。检测结果再分发给各自的后续处理流程。这种方式避免了每个摄像头都加载一份模型、占用一份显存。实际项目中16路1080p单张RTX 3060就能稳定跑10到12毫秒一帧的端到端延迟。关键是要控制队列长度队列满了就丢帧防止延迟积压。车牌识别不需要每帧都跑更合理的策略是每隔2到3帧检测一次或者在检测到车辆目标后才触发识别这样能把整体负载降一个量级。显存优化方面yolov5在FP16推理时显存占用很小但TensorRT默认会为每个执行上下文分配一定的工作空间。多路并发时建议把每路推理的maxWorkspaceSize调小到256MB避免显存碎片化。实测8路并发时显存占用可以控制在3GB以内让出更多空间给视频解码或者OCR模型。6. 真实场景里踩过的坑和排查方式6.1 夜间反光导致漏检数据增强兜底方案车牌识别系统最常见的故障场景是晚上。夜间路灯、车灯直射、车牌本身的反光涂层加在一起车牌区域对比度大幅下降yolov5漏检率明显上升。我在夜间测试时发现同一天白天的recall是98.5%到了夜间只有92%差距很大。排查过程是按链路走的。我先检查了预处理后的输入图像发现proprocessing里有两个问题一是直方图均衡化在夜间会放大噪声二是像素值归一化范围如果和训练时不一致比如训练用的是0到1推理用的是0到255会导致特征分布偏移。我统一了预处理参数后略有改善但还不够。真正起作用的是数据增强。我在训练时引入了三类夜间相关的增强策略brightness扰动在0.2到0.5范围内随机降低亮度模拟夜间整体变暗的效果。gaussian_noise和jpeg_compression模拟低光下的噪声和压缩伪影。mosaic和mixup的夜间版本从夜间样本中随机抽图混合增强模型对夜间纹理的适应能力。增强后重新训练夜间recall提升到了96.8%。和白天仍有差距但已经能配合视频流的多帧去重机制做到可接受的系统级准确率。6.2 蓝牌被识别成绿牌颜色模块的样本修正上线测试后收到最多的反馈是蓝牌怎么变成绿牌了。排查发现问题集中在黄昏和凌晨这两个时间段。这些时段环境光偏暖蓝色车牌在暖光下色调偏移接近青绿色颜色分类器的HSV兜底规则失效而模型本身的预测置信度又不够高。我花了很大功夫收集这个时段的训练样本发现一个规律黄昏时段蓝牌的G通道和B通道之间的差值会显著缩小在HSV空间表现为色相从蓝色区间漂移到青绿区间。单纯增加数据量治标不治本。最终方案是给颜色分类器增加一个时间上下文特征。具体做法是把颜色分类的输入从单帧RGB扩展到相邻三帧的加权平均因为车辆在短时间内颜色不会变而光照噪点在多帧平均后会被平滑掉。这个改动让蓝绿混淆率从4.2%降到了1.1%效果非常明显。另外我还加了一条基于业务逻辑的兜底规则如果同一块车牌在连续多帧中的颜色不一致取出现次数最多的那个颜色。视频流场景下这个投票机制几乎免费但对系统稳定性的提升是巨大的。6.3 车牌字符0/O、1/I混淆的兜底规则字符混淆是OCR模型普遍存在的问题尤其是0/O和1/I这两组。在LPRNet的混淆矩阵里这两组字符的互认概率非常高。我试过在损失函数里加惩罚项试过在训练集里人为调高这两组的样本比例效果都有但无法根治。最后落地的是三层兜底位置规则。车牌第二位是字母所以第二位的输出如果是0或1直接替换为O或I省份简称后第一位是字母规则同理。字典规则。车牌的字母部分不会出现O和I民用号牌所以除了第二位其余位置的O统一改成0I统一改成1。候选集重排序。解码时保留top3候选字符当规则冲突时用候选集中的第二个字符替代。这三层规则加在一起让字符级准确率从98.2%提升到了99.3%车牌级的整体识别准确率从92%左右提升到95%以上。对于大多数停车场和卡口场景这个水平已经具备实用价值。另外要提一嘴的是车牌识别系统这种项目数据闭环比模型结构重要得多。我后来把生产环境里OCR识别失败、人工修正过的车牌图片定期收集回来每周做一次增量训练模型效果越用越好。这个习惯比任何网络结构上的小创新都实用。如果你准备做类似项目我建议一开始就把数据回流和标注工具链设计好这会让后期的迭代效率完全不一样。本文还有配套的精品资源点击获取