ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于YOLO的车辆牌照识别系统实战:从数据到部署

2026/8/26 11:58:36 拓冰建站 浏览量
基于YOLO的车辆牌照识别系统实战:从数据到部署 简介目标检测是计算机视觉的核心任务之一其通过定位与分类技术让机器能够理解图像中的物体信息。在实际工程中目标检测模型的落地往往需要结合数据增强、模型训练与推理优化等环节。车牌识别作为典型的应用场景广泛用于停车场管理、交通监控与智慧安防其核心技术链路包含车牌定位、字符识别与后处理校验。本文以YOLOv5为检测主干结合PaddleOCR实现字符识别完整介绍了从数据集准备、标注格式转换、模型训练参数配置到ONNX部署的实践流程并针对倾斜矫正、小目标漏检、过曝干扰等常见问题给出了解决方案。无论是刚接触深度学习的新手还是需要快速构建识别系统的工程师都能从中获得一套可复用的工程方法。 最近在整理之前做过的一个车牌识别项目正好打包成了“基于YOLO的车辆牌照识别系统.zip”一口气把训练代码、标注数据、部署脚本全塞进去了。车牌识别算是目标检测领域最经典的落地场景之一停车场收费、出入口闸机、违停抓拍、高速ETC背后跑的基本都是这套逻辑。这个项目本身不算复杂但我从数据准备到模型部署把坑一个个踩完之后觉得非常值得拆开讲一讲。适合刚学完YOLO想找一个完整项目练手的同学也适合需要快速交差做个demo的工程师当然想搞懂“目标检测落地到底要经历哪些环节”的人也能从这里拿到一份完整路线。这个系统主要做的事情就三件第一用目标检测模型在画面里找到车牌的位置第二把车牌区域裁剪出来矫正角度交给字符识别模块第三输出完整的车牌字符串。整体链路不复杂但每一步都有讲究尤其是数据整理和后处理直接决定了你最终能跑到多少准确率。1. 项目整体设计与方案选型1.1 为什么用YOLO而不是传统图像处理很多人一上来就问“车牌识别不是OpenCV就能做吗边缘检测、轮廓查找、模板匹配套一套不就行了”确实在车牌位置固定、背景干净、光照稳定的场景下传统图像处理方案完全够用十几年前的老系统就是这么干的。但真实场景根本不是这样车牌可能出现在画面任意位置、角度会偏、光线忽明忽暗、雨天泥点遮挡、夜间大灯反光再加上新能源绿牌、双层黄牌这种特殊类型传统方案很容易崩。YOLO解决的是“定位”问题。它把目标检测当做一个回归任务一次前向推理同时输出目标的类别和边界框。车牌识别里最重要的一步其实不是识别字符而是先把车牌“找到”只要车牌检测得足够准后面字符识别的工作量就会小很多。这也是我坚持用YOLO而不是OpenCV做定位的原因鲁棒性完全不在一个层级上。1.2 系统整体架构与工作流程整个系统分四个模块数据层、检测层、矫正与识别层、输出层。数据层负责整理和增强车牌图片检测层用YOLO模型输出车牌边界框矫正与识别层对裁剪出的车牌做透视矫正、灰度化、二值化再用OCR提取字符输出层做后处理规则校验比如判断车牌长度、过滤明显错误的识别结果。以一张停车场入口的抓拍图为例流程是这样的摄像头采集图像先经过检测模型定位到车牌区域的坐标然后按照坐标把车牌裁下来判断是否需要做透视变换矫正紧接着用OCR引擎识别字符最后根据普通蓝牌七位、新能源绿牌八位这类规则校验结果格式合法的输出不合法的重新识别一次。整个过程单帧处理时间在CPU上大约120毫秒GPU上可以做到30毫秒以内完全满足实时性要求。1.3 技术栈选型与版本锁定项目用的是YOLOv5作为检测主模型这里先说一下为什么没有选更新的YOLOv8或者YOLOv9。核心原因是生态成熟度和部署便利性。YOLOv5的代码结构非常简洁训练、验证、导出、推理四个入口划分得很清楚.pt权重可以直接导出ONNX、TensorRT适配性强。对于车牌这种单类别目标检测任务YOLOv5s模型的精度已经足够了。YOLOv8在模块结构上做了改进比如C2f模块替换了C3训练速度和解耦头的精度确实有提升但收益相对有限。如果做一个新项目我可能无脑上YOLOv8但这个项目本身是对接老团队对方的技术栈就是YOLOv5为了后期维护和移交方便保持版本一致反而更重要。环境版本这块我用的是Python 3.8 PyTorch 1.10 CUDA 11.3这是当年亲测百试百灵的一套组合。现在新项目当然可以套Python 3.10 PyTorch 2.x但如果是复现老项目千万别手贱升级依赖Python版本一变一堆坑等着你。2. 数据集准备与预处理2.1 车牌数据的来源与类别规划车牌数据在整个项目里占的权重超过50%一个车牌检测模型效果好不好看的不是你调参多厉害而是数据够不够全面。我这份数据集主要来自两个渠道一是开源的中国车牌数据集CCPD里面有大概30万张不同场景的车辆图片二是自己补充拍摄和网上爬取的样例重点覆盖CCPD里比较缺少的场景比如夜间强反光、倾斜角度过大的、双层黄牌、港澳出入境车牌等。车牌检测模型我一开始只设了两个类别blue_plate和green_plate。后来实验发现黄色教练车车牌、白色军警车牌、黑色粤Z车牌也时有出现如果模型没学过这些就会把非蓝绿车牌的车牌区域漏检甚至误检。于是我把类别扩展为plate_blue、plate_green、plate_yellow、plate_white、plate_black共五类。多类检测的代价很小但效果收益立竿见影。2.2 标注规范与XML转YOLO格式标注工具推荐用LabelImg或者Labelme前者速度更快后者功能更全。这个项目里我用LabelImg直接输出Pascal VOC格式的XML文件。YOLO训练需要的是txt格式每行一个目标五列分别是类别id、归一化中心点x、归一化中心点y、归一化宽度w、归一化高度h。所以标注完需要做一个格式转换核心逻辑用Python写很简单import os import xml.etree.ElementTree as ET from tqdm import tqdm classes [plate_blue, plate_green, plate_yellow, plate_white, plate_black] def convert_xml_to_yolo(xml_path, out_dir, img_width, img_height): tree ET.parse(xml_path) root tree.getroot() out_lines [] for obj in root.iter(object): cls_name obj.find(name).text if cls_name not in classes: continue cls_id classes.index(cls_name) xmlbox obj.find(bndbox) x1 float(xmlbox.find(xmin).text) y1 float(xmlbox.find(ymin).text) x2 float(xmlbox.find(xmax).text) y2 float(xmlbox.find(ymax).text) # 坐标归一化中心点格式 x_center (x1 x2) / 2.0 / img_width y_center (y1 y2) / 2.0 / img_height w (x2 - x1) / img_width h (y2 - y1) / img_height out_lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) txt_name os.path.basename(xml_path).replace(.xml, .txt) with open(os.path.join(out_dir, txt_name), w) as f: f.write(\n.join(out_lines))这一段代码看着简单但有一个特别容易被坑的点图片宽高不能从XML里读必须用实际图片尺寸。因为标注软件导出的XML有时只记录文件名不记录图片尺寸你要用cv2.imread或者PIL.Image.open获取真实宽高否则归一化坐标全部出错。标注完成之后数据按8:1:1划分成训练集、验证集、测试集。每个集合的图片和txt要分开放在images和labels两个目录下YOLO代码通过路径自动匹配同名文件结构上必须严格保持一致。2.3 数据增强策略数据增强的意义在于让模型见过更多“难看的”车牌。YOLOv5自带马赛克增强、HSV色域扰动、随机翻转、随机缩放和拼接训练时默认开启但有几个参数我调过。马赛克增强默认开启用四张图片拼接到一起训练对小目标检测效果提升非常明显建议保留。HSV扰动里我把饱和度的增强幅度调大了从默认的0.0调整到了0.3因为车牌的蓝色和绿色在不同光照下饱和度差异极大这一项对颜色类别蓝牌和绿牌的区分能力影响很大。随机翻转有一点要注意水平翻转可以用但垂直翻转绝对不能用车牌一旦上下颠倒训练出来的模型就废了。另外我在项目里额外加了透视变换增强模拟车牌在画面中因为拍摄角度产生的形变。每隔几个epoch就对部分图片做一次cv2.warpPerspective让模型学会在非正视角下也能准确定位车牌。这个操作对提升复杂角度下车牌检测效果的帮助比单纯增加训练轮数有效得多。3. 模型训练与核心参数配置3.1 选择预训练权重与网络结构YOLOv5有n/s/m/l/x五个规格参数量从最小到最大差了十几倍。车牌检测目标不算太小但数量少、特征集中用不上大模型我最终用的是yolov5s.pt作为预训练权重。选择预训练权重的理由不是玄学而是迁移学习的效率优势。COCO数据集预训练的权重已经学到了通用的边缘、纹理、颜色特征车牌虽然不在COCO的80个类别里但这些底层特征是可以复用的。用预训练权重起步比从零开始训练收敛速度快得多通常训练50个epoch就能达到从零训练150个epoch的效果。如果你想尝试更轻量的部署用yolov5n.pt也可以模型体积只有4MB左右CPU实时推理没问题但检测精度会掉大概两三个点。这个取舍取决于你的部署平台项目里我保留了两个版本的训练配置方便切换。3.2 训练参数配置详解训练入口是YOLOv5项目下的train.py我的核心命令是python train.py \ --data dataset/plate.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --device 0 \ --workers 4 \ --cache这里每一个参数背后都有讲究。--img 640是输入分辨率这个值不是越大越好。分辨率越高小目标检测能力越强但训练显存占用和推理耗时也成倍增加。车牌区域在整张图中通常占5%到15%640的分辨率下已经足够再高收益不大。--batch 16是单卡能承受的上限我的显卡是8G显存batch再大就报OOM了。如果你显存更大可以往上调batch从16增加到32训练稳定性会更好尤其是BN层的统计量更准确。--cache参数会把图片提前缓存进内存省去每次迭代时从磁盘读图的I/O瓶颈。如果你的内存足够建议加上训练速度能提升30%左右。数据集配置文件长这样# dataset/plate.yaml train: dataset/images/train val: dataset/images/val test: dataset/images/test nc: 5 names: [plate_blue, plate_green, plate_yellow, plate_white, plate_black]3.3 训练过程监控与指标解读训练过程不能只看loss更要关注验证集的指标。我每次训练都会开着TensorBoard实时观察val/box_loss、val/cls_loss、metrics/precision、metrics/recall和metrics/mAP_0.5这几条曲线。一个比较典型的情况是训练集的loss持续下降但验证集的mAP一直横盘震荡这说明模型开始过拟合了训练集里背下来了但遇到新数据就抓瞎。这时候正确处理方式是提前终止训练取验证集上mAP最高的那个权重文件YOLOv5会自动保存best.pt而不是等满额epoch跑完。我的实验结果是一组比较漂亮的数据在自建的20000张测试图上车牌检测的mAP0.5达到了98.6%mAP0.5:0.95达到了87.2%蓝牌和绿牌的检测精度几乎持平。这个成绩基本可以满足实际场景需求。单类别检测任务有一个好处类别之间没有混淆mAP几乎完全取决于定位精度。如果出现mAP不高的情况大概率是标注框本身画得不准这个时候先回头修数据别调参。3.4 一次完整的训练经验复盘100个epoch的训练我跑了大概6个小时。前20个epoch是模型快速收敛阶段loss从0.1左右快速掉到0.0330到60个epoch进入缓慢优化期mAP从80%慢慢爬到95%60个epoch之后精度提升就非常有限了主要是在涨mAP0.5:0.95这种严格指标。我对比过从零训练和加载预训练权重的效果从零训练的前20个epoch基本是在浪费时间loss下降极慢而且容易陷入局部最优。加载预训练权重之后模型在第5个epoch左右就能看到明显的检测效果。如果不是做学术研究搞对比实验生产项目里永远优先选择预训练。4. 车牌矫正与字符识别模块4.1 检测到车牌之后的处理流程检测模型输出的是车牌的矩形框但要注意这个框是“轴对齐”的而车牌在画面中通常是倾斜的。直接把这个倾斜的区域裁剪出来送进OCR识别准确率会大打折扣。所以正确的做法是先做透视矫正。我的矫正方案是把检测出的矩形框先向外扩10%到20%的边距确保完整包含车牌边缘然后用cv2.getPerspectiveTransform计算变换矩阵把倾斜的目标拉正。具体代码import cv2 import numpy as np def align_plate(image, box): # box: 检测框坐标 [x1, y1, x2, y2] x1, y1, x2, y2 [int(v) for v in box] # 向外扩边距避免边缘截断 w, h x2 - x1, y2 - y1 x1 max(0, int(x1 - w * 0.15)) y1 max(0, int(y1 - h * 0.15)) x2 min(image.shape[1], int(x2 w * 0.15)) y2 min(image.shape[0], int(y2 h * 0.15)) plate_roi image[y1:y2, x1:x2] return plate_roi这是最朴素的裁剪方式只做了等比扩边。如果检测框本身就比较准这种方案够用。但更好的方案是训练四个关键点回归预测车牌的四个角点再根据角点做透视变换能处理的倾斜角度更大。这个关键点检测算是目标检测之外的额外分支项目里作为进阶优化项实现了普通场景用前一版就够。4.2 字符识别的两种实现路径识别车牌字符有两条路线我都在项目里跑通了。第一条路线是端到端用YOLO直接检测单个字符。把每个字符当成一个类别中国车牌字符有省份汉字、字母、数字总共约70个类别。这样做的好处是流程短识别和定位一步到位但问题是汉字字符在低分辨率下容易被漏检而且标注工作量极大要把每张图里的每个字符都框出来。第二条路线是级联检测车牌后裁剪区域交给OCR。我用的是PaddleOCR的检测加识别模型先把车牌里的字符框找出来再逐个识别。PaddleOCR对印刷体字符的识别准确率非常成熟字体规范的车牌字符识别率可以做到99%以上。实际项目里我选的是第二条路线投入产出比最高。PaddleOCR自带方向分类器遇到倒置的车牌会自动矫正方向对双层车牌也有默认处理逻辑。4.3 字符后处理规则OCR输出之后不能直接返回必须加一层规则校验。中国车牌有固定格式普通蓝牌是“省份汉字字母5位数字字母”新能源绿牌多一位是“省份汉字字母6位数字字母”军用白牌、警用白牌还有额外格式。我的规则校验函数做了几件事第一步检查字符数量不在长度范围直接判识别失败第二步校验字符组成第一位必须是合法省份汉字第二位必须是字母后面是字母和数字的混合第三步用训练好的混淆矩阵纠正容易识别错的内容比如字母O和数字0、字母I和数字1在低分辨率下几乎无法区分需要结合上下文过滤。这一步能挡住非常多的识别噪音。实测中OCR裸识别准确率在93%左右加了规则校验之后整体准确率提升到98.7%这个差距对于停车场计费系统来说就是天壤之别。5. 部署实现与关键经验5.1 视频流实时检测的完整实现部署是项目落地的最后一公里。我写了一个独立的推理脚本用OpenCV读取视频流喂给YOLO模型检测车牌然后调用识别模块最后在画面上叠加结果并输出。import cv2 import torch from paddleocr import PaddleOCR device torch.device(cuda if torch.cuda.is_available() else cpu) model torch.hub.load(ultralytics/yolov5, custom, pathruns/train/exp/weights/best.pt, force_reloadTrue) model.to(device).eval() ocr PaddleOCR(use_angle_clsTrue, langch, show_logFalse) def process_frame(frame): # 检测车牌 results model(frame, size640) boxes results.xyxy[0].cpu().numpy() for box in boxes: x1, y1, x2, y2, conf, cls box if conf 0.5: continue plate_roi align_plate(frame, [x1, y1, x2, y2]) # OCR识别 result ocr.ocr(cv2.cvtColor(plate_roi, cv2.COLOR_BGR2RGB), clsTrue) if not result or not result[0]: continue plate_text .join([line[1][0] for line in result[0]]) # 规则校验 plate_text validate_plate(plate_text) cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(frame, plate_text, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) return frame这个脚本里有一个容易忽略的点PaddleOCR在CPU上的推理速度略慢第一次调用还会加载模型耗时比较长。实测下来单帧OCR识别在CPU上要80毫秒加上检测的30到40毫秒整体在120毫秒左右。如果要跑满25帧以上的视频流建议把OCR模块放到独立线程里用队列做缓冲不要阻塞主循环。5.2 模型导出与ONNX部署如果要做跨平台部署不能一直依赖PyTorch的torch.hub需要把权重导出成ONNX格式这样不管是用C、Java还是Go都能通过ONNX Runtime推理。python export.py \ --weights runs/train/exp/weights/best.pt \ --img 640 \ --batch 1 \ --include onnx导出中间碰到过一个小问题PyTorch版本和ONNX导出器的opset版本不匹配导致导出的ONNX模型在ONNX Runtime里报算子不支持。解决办法是在export.py里显式指定--opset 11。实际项目的服务端推理最终用的是ONNX Runtime比直接跑PyTorch快10%到20%。5.3 压缩包交付时的工程化规范这个项目是打成zip交付的所以目录结构必须清晰到别人拿到就能跑。我最开始的目录很乱后来被迫整理了好几版最终采用这样的结构plate_system/ ├── dataset/ # 数据集 │ ├── images/ │ │ ├── train/ │ │ └── val/ │ ├── labels/ │ │ ├── train/ │ │ └── val/ │ └── plate.yaml ├── weights/ │ ├── best.pt │ ├── best.onnx │ └── yolov5s.pt ├── scripts/ │ ├── xml_to_yolo.py │ ├── train.sh │ ├── detect.py │ └── export_onnx.sh ├── requirements.txt ├── README.md └── 说明文档.pdfREADME.md里要写清楚环境的安装方式、训练命令、推理命令、常见问题。我吃过一个亏有次交付项目没写清楚Python版本结果对方用Python 3.11跑PyTorch 1.10直接报错白白折腾了半天。所以requirements.txt里最好锁定主依赖的版本号然后在README里用加粗写明测试通过的Python版本。6. 常见问题与排查技巧6.1 训练时mAP为0的排查训练刚起步时发现mAP一直为0这是新手最常见的困惑。如果验证集一直显示0优先检查三件事。第一标注的txt文件是不是空的用wc -l数一下每个txt的行数很多标注软件导出时漏写了标签第二检查类别id是否在nc范围内比如你的数据集用了nc: 5但标注里出现了class_id: 6模型直接报错或者丢弃第三检查YAML配置里names的顺序是否和标注的类别id对应顺序反了模型就全学歪了。6.2 小目标与远距离车牌漏检怎么解停车场入口倒是还好中远距离场景下车牌区域可能只有20乘40像素在640分辨率下算很小的目标。漏检原因之一是模型没有看过足够多的小车牌样本解决思路是切图训练把大图切成若干小块让车牌在训练图中占比变大。另一个是锚框尺寸不匹配虽然YOLOv5会自动计算锚框但你可以在训练前手动设置--noautoanchor用anchors参数指定一组更匹配车牌宽高比车牌大约是3比1到4比1的初始锚框。6.3 蓝牌绿牌颜色过曝导致检测失败夜间场景下大灯直射会让车牌的蓝色或绿色几乎变成白色模型很容易把这种图漏掉。数据上要加入大量过曝和低照度的样本训练参数上我之前调高了HSV饱和度增强目的就是让模型看到更多颜色失真但结构完整的车牌。还有一种特殊情况是反光造成的高光区域覆盖了半个车牌这种几乎不可能靠检测模型解决只能依赖多帧融合把连续几帧的识别结果做一个投票取出现次数最多的结果作为最终值。6.4 推理速度不达标时的优化思路如果你部署的CPU性能一般推理一帧要超过200毫秒就有必要做几个针对性的加速措施。模型层面把--img从640降到416或换用yolov5n规格的模型推理时间直接降一半推理层面ONNX模型开启动态量化或者干脆用OpenVINO跑CPU推理比ONNX Runtime还要快30%以上工程层面对视频流做抽帧不用每帧都处理比如每两帧识别一次停车场的车辆运动速度很慢完全够用。我在一次户外停车场项目里就是把检测尺寸降到了480再配合隔帧处理最终在Jetson Nano这种低算力设备上跑到了15帧每秒准确率相比640分辨率只掉了1.5个点左右完全在可接受范围。车牌识别这个项目说到底拼的不是模型结构有多新而是数据覆盖面和工程细节。每一步单独拎出来都不难但串起来就特别考验一个人的全局意识。我在做这个项目的过程中最大的体会是一定要舍得在数据整理上花时间数据好后面的训练和部署都会顺很多。如果你也准备做一个类似的识别系统建议先把工作重心放在采集真实场景图片和规范标注上模型结构反而是最不需要纠结的部分。最后再分享一个实用小技巧训练结束后记得把runs/train目录下的args.yaml文件保留好这个文件记录了你训练时的所有参数两个月后想复现模型效果直接看它就能精确还原环境比任何文档都好使。本文还有配套的精品资源点击获取