ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

YOLOv5+PPOCR车牌识别实战:从模型训练到业务落地的完整指南

YOLOv5+PPOCR车牌识别实战:从模型训练到业务落地的完整指南

直接把车牌识别系统塞进业务流里这事儿,我最近算是摸出了点门道。说白了就是一套组合拳:用YOLOv5先把车牌的位置从画面里抓出来,再交给PPOCR去抠文字。这套方案在真实业务场景里确实够顶,不是实验室里跑个demo自嗨那种,是真能扛住早晚高峰停车场出入口、物流园区道闸那种复杂光线的实战环境。咱们先看个直观的例子:某园区入口的监控画面里,一辆SUV在逆光条件下缓缓驶入,车身颜色和水泥地面几乎融在一起,但YOLOv5的检测框依然稳稳地锁住了前车牌的位置,裁剪出来后PPOCR在不到200毫秒内输出了“粤B·D32K7”这串字符。整个过程从图像输入到结构化结果输出,耗时不到300毫秒。

如果你正准备在项目里接车牌识别,或者已经在用传统图像处理方案被各种光照、倾斜、模糊问题折磨得头疼,那这篇文章应该能帮你少踩不少坑。我不打算讲什么高深理论,就从头到尾聊聊怎么把这套YOLOv5 + PPOCR的流程真正跑起来,包括模型怎么选、参数怎么调、代码怎么组织、业务流怎么接,以及我在实际项目中遇到的那些磨人的问题。

1. 整体方案设计与选型思路

1.1 为什么是YOLOv5 + PPOCR,而不是一个端到端模型

先回答一个大家都会问的问题:现在车牌识别不是有很多现成的端到端模型吗?比如直接输入一张图,输出车牌字符串,一步到位。说实话我也试过,但最终在业务落地时还是选择了两段式方案。

端到端模型的最大问题是"不可控"。你扔进去一张图,它吐出来一个结果,但中间过程完全是个黑盒。车牌没识别出来的时候,你根本不知道是车牌定位失败还是文字识别失败,排查问题全靠猜。而两段式方案把整个流程切成两个清晰独立的环节:检测负责回答"车牌在哪",识别负责回答"车牌是什么"。哪个环节出问题,直接在那个环节调试就行。

另一个现实因素是数据。端到端模型训练需要的是"整张图+车牌字符串"的标注数据,而两段式方案中,检测模型只需要车牌位置框的标注,识别模型只需要车牌字符的标注。实际项目里,收集标注数据的成本天差地别。很多业务方手里有大量的监控截图,找人标注一个矩形框比标注一串字符容易太多了。我在某物流园的项目里,就是先用已有的车辆图片标注检测框,快速把检测模型跑起来,再用检测结果自动裁剪车牌区域,半自动地构建识别模型的数据集,整个数据流转非常顺畅。

1.2 方案适用边界:什么场景最吃这套组合拳

这套方案最适合的场景是固定机位、可控视角的卡口类业务:停车场出入口、园区道闸、高速收费站、加油站入口等。这些场景有几个共同特点:相机位置基本固定,车牌相对画面的大小和位置变化范围有限,车辆行驶方向相对单一。这些特点让YOLOv5的检测压力大大降低,识别准确率也更有保障。

如果你的业务场景是那种完全不可控的视角,比如城管用的车载巡逻相机、手机随手拍的路边违停照片,车牌在画面里的尺度和旋转角度变化幅度巨大,那这套方案的准确率会明显下降。不是说不能用,而是需要投入更多精力去扩充训练数据、调优参数,甚至要考虑是否引入车牌角点回归等额外机制。所以立项之前,先想清楚场景的复杂程度,能省下后面大量的调优时间。

1.3 技术选型的三个关键取舍

第一个取舍是检测模型选择YOLOv5而不是YOLOv8或更新的版本。选YOLOv5不是因为它最好,而是因为它最"稳"。用了三年多的模型,社区积累了海量的教程、预训练权重和踩坑案例,遇到任何报错都能搜索到解决方案。YOLOv8在检测精度上确实有提升,但对硬件的要求也更高,而且部分部署工具链的兼容性还需要时间沉淀。在车牌这种目标小、场景相对固定的任务里,YOLOv5的精度已经绰绰有余。

第二个取舍是识别模型选择PPOCR。PPOCR的核心优势在于它对中文、英文、数字混合场景的支持非常成熟,而且自带方向分类器,能自动矫正一些轻度倾斜的文本区域。车牌的字符排列虽然相对规整,但各地车牌有蓝底白字、黄底黑字、新能源绿底黑字、使馆黑底白字等多种样式,字符和背景的对比度差异很大。PPOCR在这种复杂文本区域上的鲁棒性,确实比通用的OCR方案要强不少。

第三个取舍是推理框架用官方原版还是导出TensorRT。前期调试阶段,直接用PyTorch的原始模型是最省事的,改代码方便、调试信息完整。模型跑通、参数调优完成之后,再考虑性能优化。要上生产环境了,才需要导出TensorRT加速。

2. 环境搭建与模型准备

2.1 软硬件环境清单

先说硬件。我踩过的配置组合里,最省心的是一张NVIDIA T4或RTX 3060级别的显卡,显存8G起步。YOLOv5s检测模型加PPOCR的识别模型,两张模型同时常驻显存,大约占用2.5G到3G显存。当然纯CPU也能跑,就是速度感人,一张1080P图片从检测到识别完整跑完大约要2到3秒,做离线批量分析勉强能忍,实时视频流就别想了。

操作系统建议用Ubuntu 20.04或22.04 LTS,CUDA 11.7以上版本搭配cuDNN 8.4以上版本。这里强调一下,CUDA、cuDNN和PyTorch的版本必须严格匹配,不要各装各的。我之前有一次把CUDA升级到12.0,结果项目的虚拟环境里还是PyTorch 1.12的旧版本,编译扩展的时候各种报错,折腾了大半天才定位到是版本不匹配的锅。新手最容易在这个环节栽跟头,建议直接用虚拟环境管理工具,把整个工具链的版本固定下来。

2.2 模型权重获取与校验

YOLOv5部分,官方仓库提供的预训练权重,比如YOLOv5s.pt,是在COCO数据集上训练的,里面有车牌这个类别吗?没有。所以必须要使用自己的数据集进行微调,或者使用目标检测公开数据集里的车牌类别模型。这里有个更高效的做法:直接找开源的、已经训练好的车牌检测权重作为起点,然后用自己业务场景的图片做增量训练。别从零开始训练,从零训练一个检测模型至少需要上万张标注图片和大量时间,增量训练可能几百张就能收敛。

拿到预训练权重后,一定要做一个验证动作:把自己的测试图片跑一遍,看看检测效果和预期差距有多大。这一步能帮你判断现有权重的基础能力,也能在后续微调时作为对比基准。

PPOCR部分,用官方发布的中文识别模型就行。注意区分轻量版和服务端版,轻量版模型文件只有十几MB,单张识别速度快,适合实时场景;服务端版精度更高,但模型体积和耗时都上去了。我的建议是先用轻量版跑通流程,精度不够再换服务端版。

2.3 依赖安装的几个坑

依赖安装看着简单,实际容易出幺蛾子。YOLOv5的requirements.txt里会装一堆包,其中比较容易出问题的是OpenCV。系统自带的OpenCV版本和PyTorch的某些操作可能冲突,推荐在虚拟环境里单独安装openccontrib-python-headless版本,避免和系统的桌面环境抢占显卡资源。

PPOCR的老版本依赖里有个叫lmdb的库,在Python 3.10以上环境编译时经常报错。解决方案是说简单也简单:升级到新版PPOCR,或者手动编译lmdb。另外,如果GPU显存不够,建议把PPOCR的推理参数里的use_gpu设为False,让识别走CPU。别觉得这样会慢很多,PPOCR的模型本身不大,CPU推理一张车牌字符区域大概也就几十毫秒,完全在可接受范围内。整体流程的瓶颈往往在检测阶段而非识别阶段,这个结论我在多个项目里验证过。

3. 核心流程实现:检测、裁剪、识别一条龙

3.1 YOLOv5检测车牌位置的完整流程

检测环节的核心代码逻辑并不复杂,但有几个关键细节直接决定后续识别的效果。先看代码:

import cv2 import torch import numpy as np # 加载模型 model = torch.hub.load('ultralytics/yolov5', 'custom', path='best.pt', force_reload=False) model.conf = 0.35 # 检测置信度阈值 model.iou = 0.45 # NMS的IoU阈值 model.classes = [0] # 只保留车牌类别,如果有多个类别按实际调整 model.max_det = 10 # 一帧最多检测的目标数

先重点说置信度阈值。这个参数我调试的次数最多,经验值是0.3到0.4之间。调太低了,画面里的一些反光区域、广告牌上的文字区域会被误检成车牌,后续识别环节白忙活;调太高了,一些稍微模糊或倾斜的车牌会被漏检。实际项目中,我一般先设0.35跑一批历史图片,看漏检率和误检率的平衡点在哪里,再微调。

推理的时候有几个容易忽略的设置。比如检测时要把输入图片的尺寸控制在一定范围。YOLOv5默认会等比例缩放输入图片到640x640,如果原始图片是1920x1080,检测小目标车牌时,缩放后的车牌区域只有十几个像素,检测框定位精度会受到很大影响。这里我的做法是先把画面里可能包含车牌的感兴趣区域(ROI)裁剪出来,再送入检测模型。在固定机位场景下,车道位置是基本不变的,完全可以先把车道区域抠出来,再进行检测。这样做不仅提升了检测精度,还减少了计算量,实测单帧检测耗时能下降将近一半。

3.2 车牌区域裁剪与预处理

检测模型输出的是一组边界框坐标,接下来要做的就是把原始图像里的车牌区域裁剪出来,送入识别模型。这一步的关键在于:不要直接拿原始检测框的坐标去裁剪,要做适当的外扩。

def expand_bbox(img, bbox, expand_ratio=0.2): x1, y1, x2, y2 = bbox w, h = x2 - x1, y2 - y1 dx, dy = int(w * expand_ratio), int(h * expand_ratio) x1 = max(0, x1 - dx) y1 = max(0, y1 - dy) x2 = min(img.shape[1], x2 + dx) y2 = min(img.shape[0], y2 + dy) return x1, y1, x2, y2

外扩比例我习惯设0.2到0.3。为什么需要外扩?因为检测框通常紧贴着车牌边缘,但车牌本身的边缘字符经常会贴得很近,裁剪得过紧可能导致字符被切掉一部分。外扩之后,识别模型看到的上下文更完整,识别准确率会明显提升。我自己测过,外扩比例从0增加到0.2,识别准确率能提升将近4个百分点,而代价仅仅是多了几次无效的空白区域计算。

裁剪之后建议做一步简单的图像预处理:把裁剪区域缩放到统一高度,比如32像素或48像素,宽度按等比例缩放。PPOCR对输入图像的尺寸有一定要求,统一尺寸能减少识别模型的性能波动。

3.3 PPOCR文字识别

PPOCR的代码简洁直接:

from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, # 启用方向分类器 lang='ch', use_gpu=True) def recognize_plate(crop_img): result = ocr.ocr(crop_img, cls=True) if not result or not result[0]: return None, None # 提取文本和置信度 texts = [] confs = [] for item in result[0]: text = item[1][0] conf = item[1][1] texts.append(text) confs.append(conf) return texts, confs

方向分类器要不要开?建议开着。车牌本身是水平排列的,但相机安装角度或车辆行驶姿态偶尔会让车牌在画面里出现轻度倾斜。方向分类器会先判断文本区域的方向,必要时自动纠正,然后再做文字识别。这个环节对倾斜场景的帮助非常大。

识别完成之后,得到的字符串往往带有干扰字符,比如车牌边框的噪点可能被识别成"·"或"|"等符号。这里需要加一个后处理函数,做标准化清洗:

import re def normalize_plate(text): # 去除车牌的干扰字符 text = re.sub(r'[·|\s\-_.]', '', text) # 只保留字母、数字和省份简称汉字 text = re.sub(r'[^\u4e00-\u9fa5A-Za-z0-9]', '', text) return text

后处理这步看似简单,实际上对最终准确率的提升非常关键。我接手的一个项目里,原始识别准确率只有78%,加了这层清洗之后直接涨到了91%。车牌里常见的干扰字符其实就那几类,挨个清理掉就行。

3.4 完整串起来的Demo

下面把检测、裁剪、识别整个流程串起来,形成一个可以直接跑的脚本:

import cv2 from paddleocr import PaddleOCR # 初始化YOLOv5 model = torch.hub.load('ultralytics/yolov5', 'custom', path='best.pt', force_reload=False) model.conf = 0.35 model.iou = 0.45 model.classes = [0] model.max_det = 10 # 初始化PPOCR ocr = PaddleOCR(use_angle_cls=True, lang='ch', use_gpu=True) def process_plate(img): # 1. 检测 results = model(img) boxes = results.xyxy[0].cpu().numpy() plates = [] for box in boxes: x1, y1, x2, y2, conf, cls = box if conf < model.conf: continue # 2. 外扩裁剪 x1, y1, x2, y2 = expand_bbox(img, (int(x1), int(y1), int(x2), int(y2))) crop = img[y1:y2, x1:x2] # 3. 识别 texts, confs = recognize_plate(crop) if texts: plate_text = normalize_plate(texts[0]) plates.append({ 'bbox': (x1, y1, x2, y2), 'text': plate_text, 'conf': confs[0] }) return plates if __name__ == '__main__': img = cv2.imread('test.jpg') img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) plates = process_plate(img) for p in plates: print(f"车牌: {p['text']}, 置信度: {p['conf']:.2f}")

注意这里用到了cv2和torch,记得在文件开头导入,完整代码里别忘了。另外输入图片做了BGR转RGB,是因为YOLOv5的预处理默认使用RGB格式,而OpenCV读进来是BGR,不转换的话颜色通道会翻转,检测效果会受影响。这个坑很多新手会踩,转换之后检测稳定度确实有明显提升。

4. 业务流整合与性能优化

4.1 高并发场景的处理方式

如果你的业务是把这套车牌识别能力封装成HTTP服务,供多个业务系统调用,那就要考虑高并发的问题。最简单的做法是用FastAPI或Flask起一个轻量服务,但在GPU推理的场景下有个隐患:多个请求同时到达时,如果每个请求都独立触发一次模型推理,GPU资源会被频繁抢占,反而导致整体吞吐量下降。

更稳妥的做法是引入一个简单的队列缓冲机制。可以在服务里维护一个先进先出的任务队列,请求进来后先入队,后台有个单线程从队列里取任务,批量送进GPU推理。这里说的"批量"有两种含义:一是把多张图拼成一个batch送入模型,YOLOv5和PPOCR都支持batch推理;二是即便不拼batch,串行处理也比并行抢GPU要稳定得多。我实测过,一个T4显卡,单请求推理耗时约80毫秒,QPS到15的时候延迟就会飙到300毫秒以上;而用队列串行处理后,QPS稳定到20,平均延迟反而维持在100毫秒以内。这个反直觉的结论值得留意。

另外要特别注意线程安全。PaddleOCR和YOLOv5的模型对象在多个线程里共用时,偶尔会遇到内存访问冲突的问题。保险的做法是用线程锁把模型推理的代码段保护起来,或者每个线程维护一个独立的模型实例。每个模型实例占用的显存不算大,多开几个进程或者初始化几个实例,换来的是稳定性的提高。

4.2 推理速度拆解与瓶颈定位

在生产环境上线之前,一定要做一次完整的推理耗时拆解,看看时间到底花在哪里。拿一个1080P的视频帧为例,我用T4跑过一组数据:

  • YOLOv5检测:约35毫秒
  • 车牌区域裁剪与预处理:约4毫秒
  • PPOCR识别(含方向分类):约50毫秒
  • 后处理与逻辑组装:约1毫秒

整体单帧耗时接近90毫秒,理论上可以达到10FPS的处理速度。对道闸、停车场这种场景来说完全够用,但如果要处理连续视频流,且相机画面里有多个车牌,耗时还会增加。这时候的优化优先级很明确:优先优化PPOCR的识别速度。

PPOCR推理速度偏慢主要卡在方向分类器上。方向分类器虽然是轻量模型,但也需要额外的推理开销。如果业务场景是固定相机、车辆基本正向行驶,可以考虑把use_angle_cls设为False,跳过方向分类这一步。实测这个开关能省掉大约20毫秒的耗时。当然,牺牲的是对倾斜车牌的识别能力,具体取舍看你的场景需求。

4.3 生产环境部署的一些建议

模型验证完成后,速度优化还需要考虑用TensorRT加速。TensorRT可以把PyTorch模型转换为高度优化的推理引擎,在T4上能把YOLOv5的检测耗时从35毫秒压到大约15毫秒,PPOCR的识别也有30%左右的提升。转换过程非常痛苦,各种版本兼容问题、插件缺失问题层出不穷,但一旦调通了,性能提升是实实在在的。

部署方式上,推荐用Docker打包整个推理环境。把CUDA、PyTorch、YOLOv5、PaddleOCR全部塞进镜像里,业务方拿到镜像之后一键启动服务,省去配置环境的痛苦。镜像体积会比较大,大概有5到6G,但换来的是"在所有机器上表现一致"的确定性,这笔交易很划算。

5. 常见问题与排查实录

5.1 常见问题速查表

项目做完之后,我花了不少时间整理用户反馈的问题,发现高频出现的问题其实就那几类:

问题现象可能原因解决方案
检测框飘忽不定,一帧有一帧无置信度阈值太高或太低调整model.conf到0.3-0.4之间,用历史数据测试平衡点
识别结果多出干扰字符车牌边框、螺钉被识别为字符加强后处理清洗,增加正则规则过滤
新能源绿牌识别不准数据集中绿牌样本太少补充新能源车牌数据做增量训练
夜间或逆光检测不到车牌图像过暗或过曝对裁剪区域做直方图均衡化或限制对比度自适应直方图均衡化
识别结果省份简称错误PPOCR对单个汉字的识别歧义增加车牌字符集约束,校验首位字符合法性
GPU显存不足导致进程崩溃多个模型实例常驻显存减小batch size,启用内存复用,或改用CPU推理识别模型

其中"识别结果省份简称错误"这个问题需要特别展开讲一讲。全国各省市自治区的简称加起来也就几十个字,完全可以把这些汉字作为一个白名单字典,识别结果里的首位字符必须命中字典中的汉字,否则就判定为识别失败或者强制做字典匹配修正。这套规则看起来很简单,但在实际业务里能过滤掉相当大比例的骨干错误。

5.2 一个印象深刻的排查过程

有个网友一上来就发了个视频我让他测它自己的测试集,告诉我准确率奇高但线上效果一塌糊涂。排查来排查去,最后发现原因让人哭笑不得:他给的测试集图片全是白天的清晰图片,而线上业务高峰期集中在夜间,监控摄像头是红外补光模式,画面整体是灰暗偏色的,车牌区域的对比度和白天完全不是一回事。

这个案例的教训让我在后续的项目里养成了一个习惯:准备评估集的时候,强制要求业务方提供至少一个完整昼夜周期的监控数据。白天、黑夜、逆光、顺光、雨天、阴天,每个时段都要抽一些样本。宁愿评估集的准备多花两天时间,也不要上线之后再花一周去救火。

排查这类问题有一个比较实用的方法:把检测和识别两个环节的中间产物全部可视化。检测环节把框画出来,识别环节把裁剪图和OCR的置信度打出来,排查起来效率翻倍。我在项目目录里专门建了一个debug_output文件夹,每次跑完批量测试,所有中间结果都自动存档,出了任何问题回溯都非常方便。

6. 实测效果与调优记录

6.1 不同场景下的实测数据

我手头有个某园区出入口的车牌识别项目,正好可以拿来说说整体效果。这个场景的特点:双车道、互为反向、地库出入口,有闸机抬杆动作,车辆在经过时会短暂停顿,车速普遍在5到20公里每小时之间。摄像头用的是普通的网络枪机,1080P分辨率,架在距离车道约5米的地方,视角略倾斜。

拿这个场景的测试集来测,该测试集包含2300张人工标注的图片,覆盖全天各个时段。整体效果出来:

  • 检测精确率(Precision):98.1%
  • 检测召回率(Recall):96.7%
  • 车牌字符识别准确率(不考虑检测是否成功,只看裁剪区域):94.3%
  • 端到端车牌识别准确率(检测+识别全链路):91.5%

这个"端到端准确率"才是有业务意义的数字。检测出来但识别错了,或者检测漏了,业务上都算失败。91.5%的准确率已经能满足大部分场景的需求,如果用更严格的业务口径,比如"识别错误的车牌绝对不能放行进入系统",那可以通过加置信度阈值和人工复核机制来兜底。

6.2 调优过程记录

第一次跑出来的端到端准确率大概是84.7%,距离业务要求的90%有明显差距。我花了三周时间逐步调优,主要做了以下几件事:

第一步,扩充数据。把业务方过去两周的监控录像拉出来,挑选了大约5000张包含车牌的帧,交给标注团队补充标注。这部分工作耗时最长,但效果也最明显。

第二步,调整检测阈值。把置信度阈值从0.5逐步下调到0.35,漏检率明显下降,误检率略有上升,但通过后续识别环节的置信度过滤,整体准确率还是涨了。

第三步,替换识别模型。把PPOCR的轻量版模型换成服务端版,单张识别耗时增加了大约40毫秒,但车牌字符的识别准确率提升超过2个百分点。在这个业务场景里,40毫秒的延迟完全在可接受范围,这笔trade-off非常划算。

第四步,加强后处理规则。针对该园区车辆的特点,补充了几个清洗规则:去除识别结果里的"·"符号,固定第八位字符必须是数字或字母,省份简称必须命中白名单字典等。

最终全部完成后,把数据集切成训练集、验证集、测试集做最终评估,成绩单就是我上面贴的这组数字。这组数字平时吹吹牛完全够用了,但说实话在真实业务里,还需要配合一个"低置信度转人工复核"的流程,才能真正让运营方放心。

7. 一点经验沉淀

这趟做下来,我最大的体会是:车牌识别系统在业务流里落地,真正考人的其实不是AI模型的精度,而是工程化的能力。把检测、识别、后处理、队列、部署、灰度这整套东西捏合到一起,需要的是一点一滴抠出来的细节。模型精度只要不太差,完全可以通过数据补充、后处理规则、人工兜底机制来补足。

最后再分享一个小技巧:无论是YOLOv5的训练数据还是PPOCR的微调数据,当中一定要包含"几乎识别不出来"的困难样本,比如严重模糊的车牌、极端角度下的车牌、被泥水遮挡一半的车牌。我习惯在每批训练数据里掺入8%到10%这样的困难样本,虽然会让模型整体的训练损失稍微偏大,但换来的泛化能力提升会让人惊喜。真正上线之后,你会感激自己当初在困难样本上花的这些时间。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进