资讯动态

车牌识别系统实战:从YOLOv8检测到LPRNet识别的全流程落地

发布时间:2026/10/19 7:33:57 来源:尧图企业网站定制
去年接到一个停车场出入口改造的小需求客户要求很简单摄像头拍到车系统自动把车牌号显示在屏幕上进出记录要能查。我一开始想得也比较简单——车牌区域特征那么明显随便找一个现成的识别模型调用一下不就完事了真正动起手来才发现车牌识别这条路最花时间的地方不是“调用模型”而是把检测、识别、部署、界面这一整条链路串起来并且让它在各种光照、角度、天气下都不掉链子。这篇文章我就按自己实际做完这个项目的顺序来写从数据准备、检测模型训练、识别模型训练到后端接口封装和UI界面搭建再到真实场景排障一条线走完。内容会偏工程一些适合有PyTorch基础、想把“车牌检测与识别”从Demo做成可用系统的朋友参考。如果你正处于“模型能跑通但现场不好用”的阶段这里面的坑大概率你也会踩到。1. 先想清楚车牌识别为什么必须拆成“两道工序”很多人第一次做车牌识别习惯直接找一个大模型把整张车辆图片丢进去期待模型直接吐出一串车牌字符。这个思路在公开数据集上看起来可行但落到真实场景基本会失败。原因不复杂车牌在整幅画面里占比很小而背景变化极大树影、车灯、广告牌、路面反光都在干扰模型。如果让单个模型同时完成“找到车牌”和“读出字符”它需要从海量数据中同时学习两个完全不同的能力样本需求和训练难度都会成倍上升。1.1 “先检测、后识别”的流水线逻辑我把整个系统拆成了两个独立模块串成流水线第一个模块做车牌检测只回答“画面里有没有车牌、车牌在哪个位置”第二个模块做车牌识别只负责读已经裁剪出来的车牌图像输出字符序列。用一个生活化的比喻检测模块像是一个人先在大型会场里帮你找到那块写着编号的门牌识别模块则像拿着放大镜仔细读出门牌上的字。两步各管各的出了问题也容易定位——漏检了是检测模块的事读错了是识别模块的事不会糊成一团。这种两段式结构还有一个实际好处检测模块输出的带位置信息结果可以做二次筛选。比如某一帧图像里检测框置信度不高但识别模块给出的字符置信度很高那大概率也是真车牌反过来如果检测框置信度很高但识别结果字符乱七八糟那多半是误检。两个模块互相校验比单个模型直接输出结果要稳得多。1.2 技术栈与整体架构这次我最终采用的技术栈如下表所有选型都围绕“现场设备性能有限、需要实时处理、部署尽量简单”这三个约束条件。模块选型选型理由车牌检测YOLOv8sanchor-free设计免去手工调锚框参数预训练权重通用性强推理速度快车牌识别LPRNet轻量级端到端序列识别网络无需字符级分割标注适合长度固定的车牌字符后端服务FastAPI ONNX Runtime异步接口性能好模型转换ONNX后部署和升级都方便前端界面Flask HTML/JS浏览器直接访问无需安装客户端支持远程查看和多路视频数据存储SQLite单机场景下足够用零配置适合现场部署整条链路的处理顺序是摄像头视频流抽帧YOLO检测出所有车牌框根据检测框坐标从原图裁剪出车牌区域由于现场安装角度通常存在透视变形先做透视校正把车牌拉正把校正后的图像缩放成固定尺寸送入LPRNet识别字符输出结果写入数据库同时推送到前端界面展示。这个顺序里最容易被新手忽略的是透视校正少了这一步倾斜车牌识别率会明显下降后文我会专门展开讲。2. 数据准备不是凑数量从采集清洗到增强的取舍真正开始训练之后我第一个深刻体会是车牌识别项目里数据质量对最终效果的影响远超模型结构。把数据准备好后面训练阶段会顺很多数据乱糟糟换再强的骨干网络也救不回来。2.1 数据集来源与标注格式转换我的训练数据主要来自三个渠道实际监控录像抽帧、公开车牌数据集、程序合成的模拟车牌。比例控制在大约7:2:1真实数据为主公开数据做补充合成数据只用来丰富字符组合覆盖。实际录像抽帧不是随手截屏要避免相邻帧高度相似造成的数据冗余。我的做法是每秒钟最多抽2帧并且用感知哈希对抽出的帧做去重相似度高于阈值的直接丢弃。否则你会发现训练集看着有好几万张实际有效信息可能只来自几十个不同场景。公开数据集标注格式五花八门有的是VOC格式的XML文件有的是COCO格式的JSON而YOLO训练需要的是归一化的txt格式。转换脚本并不复杂核心就是把边界框从“左上角坐标宽高”转换成“中心点坐标宽高”import xml.etree.ElementTree as ET def voc_to_yolo(xml_file, img_w, img_h): tree ET.parse(xml_file) root tree.getroot() for obj in root.findall(object): name obj.find(name).text bndbox obj.find(bndbox) x1 float(bndbox.find(xmin).text) y1 float(bndbox.find(ymin).text) x2 float(bndbox.find(xmax).text) y2 float(bndbox.find(ymax).text) cx (x1 x2) / 2 / img_w cy (y1 y2) / 2 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h print(name, cx, cy, w, h)一句提醒转换时千万别把原图的宽高搞错我见过有人把长宽对调导致所有训练标签全部偏移训练出来模型检测框整体错位排查了很久才发现是这里的问题。2.2 半自动标注与坏样本清洗车牌检测的标注相对简单只需要一个类别plate画矩形框就行。但几万张图纯人工标注太累我用了一个省力的“半自动标注”流程先用一个预训练好的YOLO模型对未标注图像做预测生成预标注框再用标注工具打开逐张确认和修正。这一步建议重点关注清洗规则而不是盲目堆量。我定了几条死规矩人眼都看不清车牌的样本直接删除车牌照模糊训练只会把模型带偏车牌被遮挡超过三分之一的不进训练集放进测试集用来评估鲁棒性车牌倾斜角度超过45度的样本在没有配套矫正逻辑之前不要太多避免检测模型学出奇怪偏好单独收集一批“无车牌但容易误检”的负样本比如车头局部、轮胎、广告牌、反光金属表面负样本在总数据量里占10%左右能显著降低误检率。清洗阶段偷的懒都会在后续mAP指标和线上误检数量上还回来。1000张高质量的真实数据往往好过1万张充满噪声的低质量数据。2.3 增强策略能做什么不能做什么数据增强方面检测模型和识别模型的策略差别很大。检测模型输入的是整幅画面增强可以激进一些。我使用了Mosaic、HSV颜色扰动、随机仿射旋转限制在±10度、运动模糊。Mosaic把四张图拼成一张对小目标检测特别有效因为拼接后目标尺度缩小等效于让模型见到更多“远距离小车牌”的样本。识别模型输入的是已经裁剪好的车牌图增强就要轻拿轻放。我只做了轻微亮度扰动、对比度扰动、小幅度透视扰动和高斯噪声。因为识别模型的任务是读字符过分扭曲字符形状会起反作用。这里有一条红线新手必须记住车牌检测和识别任务里不要做水平翻转增强。车牌字符是有严格顺序的比如“苏A12345”翻转后变成“54321A苏”模型会学习到错误的方向信息。垂直翻转同理。另外车牌长宽比比较固定识别模型输入时不要粗暴resize成正方形应该等比缩放到固定高度比如48像素宽度随车牌实际比例变化再统一padding到固定宽度比如128像素否则字符被拉宽或压扁识别精度会明显下降。3. 检测模型训练实录YOLO选型到漏检难题破解检测模块是整个系统的第一道关卡它漏检了后面识别模块再强也没用。这一段我记录一下训练过程中的选型思路和几个关键调参点。3.1 为什么是YOLOv8s选检测模型时我并没有盲目追求高精度而是考虑了现场设备的实际算力。停车场道闸经常用的边缘设备性能有限Faster R-CNN那种两阶段检测器精度高但速度太慢很难做到实时。YOLOv5s虽然生态成熟但锚框需要手动调整对于车牌这种固定长宽比的矩形目标还好但遇到旋转角度大的情况表现一般。YOLOv8把anchor-free机制引入省去了聚类调锚框这一步C2f结构在同等计算量下特征表达能力更好部署生态也很完整训练好的模型可以直接导出ONNX。还有一个偷懒但很实用的原因YOLOv8的预训练权重是在大规模通用目标数据集上训过的对“类车牌”的矩形纹理区域有一定先验响应迁移到车牌检测任务时收敛速度和最终精度都更理想。我从yolov8s.pt预训练权重起步而不是随机初始化。3.2 训练配置与Loss曲线观察数据配置只需要一个简单的YAML文件path: D:/plate_data train: images/train val: images/val nc: 1 names: [plate]训练命令我做了几个关键取舍yolo detect train dataplate.yaml modelyolov8s.pt epochs150 imgsz960 batch16 device0这里最核心的参数是imgsz960而不是默认的640。原因很简单车牌在整幅监控画面里通常只占很小一块区域640分辨率下平均只有大约20×10像素特征太少识别难度大。提高输入分辨率后车牌在特征图中的尺寸变大检测召回率提升明显。代价是训练和推理速度变慢但现场场景里精度优先。Loss曲线观察上不要只盯着一张图看。我会同时看train_loss和val_loss两条曲线train_loss持续下降、val_loss先降后升就是典型的过拟合信号这时候不是继续加训练轮次而是增加数据增强强度或提前早停。验证指标方面mAP0.5很容易刷到0.98以上但那是宽松指标关键要看mAP0.5:0.95我给自己定的及格线是0.85这个指标对边界框定位质量更敏感而定位质量直接影响后续车牌裁剪结果。3.3 漏检和误检的处理路径训练完开始测试时最常遇到的就是漏检和误检。漏检集中在远距离小目标、夜间低照度车辆、大面积遮挡误检集中在车头格栅、圆形车标、反光金属物。漏检的排查顺序我总结了一个优先级先确认输入图像里的车牌实际像素尺寸。如果过小优先提高输入分辨率或者对检测框周围做二次放大检测把漏检的硬样本单独收集起来加进训练集。这里要注意不是往数据集里随便加普通样本而是专门加入“模型当前搞不定”的困难样本这叫难例挖掘工程上降低置信度阈值。我在线上把默认的0.5降到了0.3让检测框先尽量出来再用识别模块的字符置信度做二次过滤。单看检测模块误检增加了但整条链路的综合准确率其实是上升的因为误检测框在识别阶段会被字符置信度拦住。误检的处理方法前面已经提到过就是构建负样本集。还有一个容易被忽略的点NMS的IoU阈值也会影响多车牌场景。道闸前同时出现两辆车时如果IoU阈值设得太高相邻检测框可能被合并导致只输出一个框。我最终用的是NMS IoU阈值0.45置信度阈值0.3在这组参数下单帧多人多车的场景表现最稳定。4. 识别模型训练LPRNet字符序列建模与CTC解码细节检测模型解决了“车牌在哪”接下来识别模型负责“车牌是什么”。这一段是整个项目里理论密度最高的部分但只要理解了序列建模的思路实操起来并不复杂。4.1 网络结构与“端到端”原理LPRNet是经典的轻量车牌识别网络它的核心特点是不需要字符级分割标注直接吃一整张车牌图像输出字符序列。网络结构分三段CNN主干负责提取视觉特征双向RNN负责建模字符之间的序列关系最后通过全连接层输出每个时间步的字符概率分布。我用自己的语言解释一下CTC Loss在这里的作用。把裁剪后的车牌图像按宽度方向切分成许多个时间步每个时间步网络都会输出一个字符概率分布。但问题是一个字符通常横跨多个时间步怎么把一串时间步输出对应到最终的车牌文本CTC的解决办法是允许输出中出现“空白”符号也允许重复字符被合并。训练时CTC会自动穷举所有可能的对齐方式找出总概率最大的那条路径。这就像语音识别里“一个音素对应多个帧”的处理逻辑本质是一个对齐问题。核心结构的关键代码可以简化为class LPRNet(nn.Module): def __init__(self, num_classes): super().__init__() # 输入 [B, 1, H, W]H通常为48 self.backbone nn.Sequential( nn.Conv2d(1, 64, kernel_size3, stride1, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), # 再堆叠几层卷积池化逐步扩大通道 ) self.rnn nn.LSTM(128, 128, bidirectionalTrue, batch_firstTrue) self.fc nn.Linear(256, num_classes) def forward(self, x): x self.backbone(x) # [B, C, H, W] b, c, h, w x.shape x x.reshape(b, c * h, w) # 把空间特征按宽度方向压成时间步 x x.transpose(1, 2) # [B, W, C] x, _ self.rnn(x) # 双向建模序列关系 logits self.fc(x) # [B, W, num_classes] return logits注意这里按宽度方向展平作为时间步正是利用了车牌字符按水平顺序排列的先验。字符识别本质被转换成了序列标注问题神经网络天然能学到字符间的顺序依赖关系。4.2 字符集设计从蓝牌到新能源绿牌字符集设计是识别模型最容易翻车的环节。我一开始图省事只放了汉字、字母、数字的常见组合结果新能源汽车绿牌上线后一片乱码。字符集不是简单堆字符要考虑实际覆盖哪些号牌类型。我最终构建的字符表包含各省简称汉字、24个大写英文字母去掉I和O因为容易和数字1、0混淆、10个数字再额外留一个CTC空白位总类别数通常在60到70之间。如果业务场景涉及警用车、领馆车、港澳车等特殊号牌可以单独扩充字符集但扩充前提是相应样本量要足够否则模型很难学住那些生僻字符。新能源绿牌和传统蓝牌的一个典型差异是绿牌比蓝牌在第二位多了一个能源类型字母D或F整个车牌的字符排列信息不一样。训练时不需要特殊处理只要训练数据中绿色车牌足够多模型自然能学到这个规律。但推理阶段的后处理需要区分蓝牌绿牌的长度规则避免输出结果字符数不对。4.3 训练与调优字符混淆问题实战识别模型的训练样本来自检测数据集的裁剪图所以训练流程一般放在检测模型训练完成之后。训练时把车牌图等比缩放到高48像素、宽度动态调整实现时固定最大宽度不超过128像素右侧空白填充。灰度图输入即可车牌字符识别基本不依赖颜色信息反而颜色反而可能干扰模型。损失函数直接使用PyTorch内置的CTC Lossimport torch # log_probs: [T, B, num_classes] # targets: 字符索引序列 # input_lengths: 每个样本的时间步长度 # target_lengths: 每个样本的真实字符长度 loss torch.nn.functional.ctc_loss( log_probs, targets, input_lengths, target_lengths, blankblank_idx )解码阶段我推荐beam search宽度设为3虽然比贪心解码慢一点但车牌这种短字符串场景下光束搜索能通过概率累计把正确字符拉回来。贪心解码看着快遇到模糊字符就很容易错。调优阶段最头疼的是相似字符混淆问题常见组合是“0和O”“1和I”“2和Z”“5和S”。处理思路分两个层面训练层面确保训练数据里这些字符的样本均衡不要某个字符出现特别少推理层面因为民用蓝牌字母表里通常不含I和O可以在后处理里硬性把识别出的O转成0I转成1。汉字方面“皖”和“晥”这类形近字也要排查训练集保证字符表里没有标注错误的错别字。评估指标我拆成“整串正确率”和“单字符准确率”两个维度。整串正确率指的是整块车牌所有字符全部识别正确单字符准确率是每个位置字符的独立正确率。整串正确率低于96%不建议上线因为真实场景的模糊样本会把这个数字拉得更低。5. 联调与UI接口封装、前端界面与实时识别链路模型训练完成只是一个开始。从“能在命令行跑通的模型”到“现场人员能用的系统”中间还有接口封装、界面搭建、数据管理一串活。这一段是整个项目的“最后一公里”。5.1 模型部署ONNX转换与FastAPI服务直接拿PyTorch权重跑推理不是不行但现场升级麻烦而且PyTorch库体积大、启动慢。我习惯把训练好的模型先转成ONNX格式推理时统一走ONNX Runtime。YOLO模型导出一行命令yolo export modelbest.pt formatonnx imgsz960 opset12LPRNet导出时要固定输入尺寸我用的输入是1通道、48×128import torch model LPRNet(num_classeslen(charset) 1) model.load_state_dict(torch.load(lprnet_best.pt, map_locationcpu)) model.eval() dummy_input torch.randn(1, 1, 48, 128) torch.onnx.export( model, dummy_input, lprnet.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}} )后端接口我选择FastAPI核心原因有两个一是异步支持好Video帧请求并发处理时不会阻塞事件循环二是自动生成接口文档联调前端时不用反复问接口格式。一个关键的工程细节是模型加载要做成全局单例避免每个请求都重新加载权重。同时推理过程要加锁或任务队列防止高并发时多个推理请求同时挤占显存导致OOM。接口返回的JSON结构我在项目一开始就定好了规范车牌文本、检测框坐标、识别置信度、检测时间。前端和数据库都基于这个结构开发后面改动成本小很多。5.2 界面选型为什么我选择了Web前端方案UI部分我对比过三条路线PyQt桌面客户端、Streamlit快速原型、浏览器Web方案。PyQt的优点在于原生控件丰富适合单机收费岗亭这种封闭场景但打包分发麻烦现场几台电脑都要装Python环境和依赖升级版本更是噩梦。Streamlit写数据类原型非常快可它做实时视频流支持一般交互深度也受限更适合内部Demo不适合交付给客户长期使用。我最终选择了Flask加HTML/JS的Web方案。原因很直接现场任何一台电脑打开浏览器就能访问不需要安装任何客户端支持远程查看多路摄像头画面后续加人机交互模块也灵活。实时视频预览用MJPEG流实现后端不断把摄像头帧编码成JPEG拼接返回前端一个img标签就能展示app.get(/video_feed) def video_feed(): def generate(): while True: frame camera.get_latest_frame() ret, jpeg cv2.imencode(.jpg, frame) yield (b--frame\r\nContent-Type: image/jpeg\r\n\r\n jpeg.tobytes() b\r\n) return Response(generate(), mimetypemultipart/x-mixed-replace; boundaryframe)识别结果的推送我用WebSocket或者前端定时轮询接口都行。项目里我先用轮询每两秒拉一次最近的识别记录简单稳定后面要推实时告警再换成WebSocket不迟。5.3 实时识别链路的实现细节把“摄像头画面”和“识别结果”串起来有几个细节直接决定系统是否可用。抽帧频率是第一件事。摄像头一般25帧甚至30帧但车牌识别不需要对每一帧都做推理那样算力完全浪费。我的做法是每300毫秒抽一帧送推理相当于每秒3到4次识别。车辆过闸口的速度正常不会太快这个频率完全够用。结果去重是第二件事。同一辆车在岗亭前停留两秒系统可能识别到同一车牌七八次如果不做去重数据库会堆满重复记录。我在后端加了一个简单规则同一个车牌号在三秒内只记录一次。数据持久化用SQLite一张表就够id、车牌号、识别置信度、检测时间、原始图片路径、车牌裁剪图路径。为什么不用MySQL单机场景下SQLite完全够用而且零配置现场重启机器也不需要额外启动数据库服务。如果后续要接多个岗亭做集中管理再把SQLite换成MySQL也不难接口层不用动。前端界面分为三块视频预览区、识别结果列表、历史记录查询。实时识别出的车牌号和置信度在列表里滚动展示点某一条记录能看到当时抓拍的原始照片和裁剪车牌图方便事后核对。6. 实测排障夜间、倾斜、绿牌这些真实场景怎么处理把整套系统放到真实道闸现场跑才算真正开始煎熬。模型在自己电脑测试集上跑得好好的一到现场就状况百出。这一节记录的正是我踩过最深的几个坑以及对应的解决办法。6.1 夜间反光和低照度夜间是车牌识别最头疼的场景没有之一。我最开始用补光灯把现场照得很亮结果车牌反光严重字符区域被白斑盖住识别率反而下降。这里有两个层面的调整。图像层面我在推理前加了亮度自适应处理检测部分对整帧图像做分块直方图均衡化让暗部的车牌轮廓更明显识别部分对裁剪出车牌图再做一次局部对比度增强把字符和底板区分开。模型层面光靠推理时的图像增强是不够的训练数据里必须有夜间的数据分布。我在数据增强流程里主动加入了模拟夜间效果随机压低曝光、叠加乘性噪声、生成高光溢出等。训练数据覆盖了夜晚场景后模型本身对低照度就有了鲁棒性。工程参数上夜间检测的置信度阈值需要单独处理。我白天用0.3没问题但夜间有些真实车牌检测置信度会掉到0.2左右。我的做法是夜间时段把检测置信度阈值降为0.25让检测框先出来识别阶段的字符置信度会拦住大多数误检测。两级置信度配合比单一阈值更稳健。6.2 透视倾斜校正的简易实现道闸和卡口的安装角度决定了现场画面里的车牌几乎不会正对着摄像头。直接拿检测框裁剪出来送识别模型效果要大打折扣字符比例都被透视变形扭曲了。我最初测试单帧识别率只有八十几分后来加上透视校正准确率直接跳了一个台阶。具体做法是从检测框出发对框内图像用Canny边缘检测拿到车牌外轮廓再用霍夫变换找到四条边得到四个角点。如果这一步不稳定也可以退而求其次直接用检测框的四个顶点配合预设的标准车牌宽高比做透视变换。OpenCV实现很简单import cv2 import numpy as np src np.array([[x1, y1], [x2, y2], [x3, y3], [x4, y4]], dtypefloat32) dst np.array([[0, 0], [norm_w - 1, 0], [norm_w - 1, norm_h - 1], [0, norm_h - 1]], dtypefloat32) M cv2.getPerspectiveTransform(src, dst) warped cv2.warpPerspective(img, M, (norm_w, norm_h))我把校正后的图像统一填成宽150、高50的标准矩形再等比resize到识别模型的输入尺寸。这一步额外耗时大约两三毫秒但换来的是倾斜车牌识别率的明显提升。同时训练识别模型时也应该加入随机的透视扰动增强模拟现场安装角度的多样性这样模型推理时才不会对处理后的图像感到陌生。6.3 数据合规与端侧部署体会现场测试跑通之后还有两件容易被忽略的事。一是数据合规。真实车牌图像本质上是个人车辆信息的载体采集、标注、存储都需要有合规意识。项目上如果使用客户提供的监控录像要和客户明确数据用途范围对外展示识别效果时车牌号应做模糊化处理避免直接暴露真实信息。这一点不是技术问题但出事就是大事。二是部署形态。道闸现场的网络状况未必稳定整套推理链路必须支持离线运行。前端页面走内网访问识别结果先写本地数据库再通过网络同步到管理端这样断网时系统也不会瘫痪。最后列一下我整个实测周期的数据表现场景测试样本检测召回整串识别正确率平均单张耗时白天/晴天120099.2%98.3%45ms夜间/补光90096.7%95.1%48ms雨天/溅水40094.8%92.6%50ms这个成绩对常规道闸场景已经够用。如果遇到极端天气或者特殊领域车准确率还会有波动那需要靠持续收集现场困难样本、迭代模型来逐步解决。这个项目做完之后我最大的体会是换更强的模型不如把数据和前处理做扎实。一开始我花了很多时间去调检测模型的骨干网络结构mAP始终涨不上去后来静下心清理训练数据、补齐夜间增强、加透视校正逻辑效果反而立竿见影。车牌识别这个方向其实没有什么黑魔法先跑通“数据→检测→识别→界面”这个最小闭环再回头逐段做精细化优化才是比较靠谱的路线。如果你正准备做类似的项目希望这篇记录能帮你省掉一些我走过的弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑