简介:目标检测与光学字符识别是计算机视觉中两项基础技术,广泛应用于智能交通、安防监控等场景。车牌识别系统是二者的典型结合——通过目标检测模型定位车辆牌照区域,再借助序列识别模型将字符图片转换为文本。本文从工程实践角度出发,梳理了一套基于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之外还得靠自己补
车牌检测数据集最常用的是中科大的CCPD(Chinese 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_size=640,兼顾速度和精度,车牌检测不需要太高分辨率。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,输入尺寸64x64,RGB三通道,全连接层输出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-Z(I和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,一条命令就能导出ONNX:python 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或者d3d11va,CPU占用率能降到原来的三分之一。
后处理中还有一个容易被忽略的环节是颜色识别和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"统一改成"0","I"统一改成"1"。
- 候选集重排序。解码时保留top3候选字符,当规则冲突时,用候选集中的第二个字符替代。
这三层规则加在一起,让字符级准确率从98.2%提升到了99.3%,车牌级的整体识别准确率从92%左右提升到95%以上。对于大多数停车场和卡口场景,这个水平已经具备实用价值。
另外要提一嘴的是,车牌识别系统这种项目,数据闭环比模型结构重要得多。我后来把生产环境里OCR识别失败、人工修正过的车牌图片定期收集回来,每周做一次增量训练,模型效果越用越好。这个习惯比任何网络结构上的小创新都实用。如果你准备做类似项目,我建议一开始就把数据回流和标注工具链设计好,这会让后期的迭代效率完全不一样。
本文还有配套的精品资源,点击获取