ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLOv8+LPRNet车牌识别系统:双阶段检测识别与部署全解析

YOLOv8+LPRNet车牌识别系统:双阶段检测识别与部署全解析 简介基于YOLOv8与LPRNet的车牌识别项目提供完整Python源码与预训练模型适合计算机、电子等信息类专业课程设计、期末大作业或毕设参考。项目采用YOLOv8完成车牌目标检测LPRNet进行字符识别并配套Flask后端与Vue前端形成从图像处理、模型训练到界面展示的完整链路。压缩包共60个文件涵盖13个Python脚本训练、推理、数据增强与标注生成、3个PyTorch模型文件.pt/.pth、Vue前端组件、配置文档及22张示例图片整体约36MB便于直接运行与二次开发。已有1171人学习下载说明该选题具备较高的参考热度。对希望快速搭建车牌识别系统、理解检测与识别联合流程的开发者这份资源能节省环境搭建与调试时间并可通过现有代码修改实现更多功能。1. 从 YOLOv8 到 LPRNet车牌识别不是单模型而是一条双阶段流水线拿到这份基于 YOLOv8 和 LPRNet 的车牌识别系统 python 源码第一反应是把「识别」当成一个模型的事。实际跑通后你会发现整条链路是 YOLOv8 先负责在第一阶段把车牌从整张图中框出来LPRNet 再在第二阶段把框内的字符序列读出来。这个「先检测、后识别」的拆分让每个模型都只干一件事检测器不关心字符是什么识别器不关心车牌在哪里。项目里同时带了 Flask 后端、Vue 前端、仿真数据生成脚本和训练好的权重适合做课程设计、期末大作业或者毕设 baseline也适合想从零理解检测加识别完整流程、又不想从采集数据开始的初学者。本文会按「架构 → 数据 → 训练 → 部署排错」的顺序把每一步的坑和参数讲透。2. 双阶段架构与代码地图从 Flask 启动到 Vue 上传的完整闭环2.1 为什么把检测和识别拆成两个模型而不是一个端到端网络如果只用单个模型做端到端车牌识别最常见的做法是直接把整张图输入让网络输出一串字符。但这里有个现实问题车牌在画面里往往只占很小一块区域背景中的干扰远多于字符本身单模型的收敛难度和推理开销都会变大。把问题拆成「定位 读字」之后检测器只需输出四个坐标识别器输入的是被裁剪后的车牌小图字符占比高、背景干净识别精度自然更容易拉起来。这个项目里检测器选的是 YOLOv8识别器选的是 LPRNet。YOLOv8 属于一阶段检测器在精度和速度之间平衡得很稳训练自定义数据集时只需要标准 YOLO 格式的 txt 标注LPRNet 则是一种轻量序列识别网络输入定长的车牌图输出不定长的字符序列不依赖外部解码器。两段串联后整条链路的耗时主要取决于检测器推理一次识别器处理的是几十像素的小图开销很小。这也是它能作为课程设计 demo 直接跑起来的原因。2.2 源码目录逐块拆解与启动链路解压之后目录结构值得先花两分钟摸清。项目主体分三块Flask 后端app.py model runs uploads、LPRNet 训练与数据生成generate_lpr_data.py、batch_resize_images.py、create_annotations.py、split_dataset.py、main.py、Vue 前端src、vite.config.js、package.json、public、index.html。目录/文件作用使用阶段app.pyFlask 入口提供上传与推理接口部署/联调model/output_lpr训练好的 LPRNet 权重与推理输出部署/联调runsYOLOv8 训练输出权重、指标曲线、验证结果训练后查看uploads前端上传的图片暂存目录部署/联调LPRNet/main.pyLPRNet 训练主脚本复训识别器generate_lpr_data.py仿真车牌数据生成构造训练集batch_resize_images.py批量缩放图片数据预处理create_annotations.py生成 YOLO 格式标注数据预处理split_dataset.py划分训练/验证集数据预处理images_lpr、config识别器数据目录与训练配置训练前调整srcVue前端页面与接口封装联调启动链路也不复杂先启动 Flask 让后端监听端口再跑 Vue 开发服务器浏览器里上传一张带车牌的图片经过后端推理返回车牌号和坐标前端渲染结果。训练链路是另一条线先用脚本生成仿真车牌图标注后喂给 YOLOv8 训检测器同时用更密集的车牌字符图喂给 LPRNet 训识别器两套权重备好后Flask 推理时依次调用。提示README.md 里写了环境依赖和启动顺序第一次跑先按 README 装依赖、起后端再起前端省得来回排查。3. 仿真数据生成用四条脚本造出 YOLOv8 能直接吃的数据集3.1 generate_lpr_data.py从字体渲染到车牌合成的完整流程训练车牌检测器最头疼的是数据。真实车牌涉及隐私和采集成本很难在课程设计里攒够几百张带标注的图所以项目里的思路是仿真先用程序生成带车牌字符的底图再贴到自然背景上。generate_lpr_data.py 干的就是这件事。常见做法是定义字符集省份简称 字母 数字随机组合成车牌号按真实车牌的底色和排版渲染到一张小图上然后用随机位置、随机旋转和透视变换贴到背景图里。脚本里通常会有几个可调参数比如车牌字符个数、贴图数量、背景图目录、输出目录。# generate_lpr_data.py 关键逻辑按常见实现还原 import random from PIL import Image, ImageDraw, ImageFont PLATE_CHARS 京津沪渝冀豫云辽黑湘皖新蒙琼川贵桂甘陕青藏吉宁苏浙赣鄂闽粤 ALPHANUM ABCDEFGHJKLMNPQRSTUVWXYZ0123456789 # 去掉易混淆的 I/O def generate_one_plate(font_path, output_size(240, 80)): # 随机生成 7 位车牌号省份简称 发牌机关字母 5 位序号 plate_no random.choice(PLATE_CHARS) random.choice(ALPHANUM) plate_no .join(random.choices(ALPHANUM, k5)) img Image.new(RGB, output_size, (0, 0, 255)) # 蓝底车牌 draw ImageDraw.Draw(img) font ImageFont.truetype(font_path, 48) draw.text((10, 10), plate_no, fill(255, 255, 255), fontfont) return img, plate_no这段代码的核心参数需要解释。PLATE_CHARS 是省份简称集合ALPHANUM 特意去掉了 I 和 O因为这两个字符和数字 1、0 在仿真渲染里视觉上极易混淆训练时会引入噪声output_size(240, 80) 是生成的车牌底图尺寸一般按国内蓝牌约 3:1 的长宽比设定。通过修改 font_path 和字号可以控制字符粗细和边缘效果这决定了仿真图接近真实车牌的程度。实际使用时字体建议直接用 Windows 自带的黑体或者思源黑体笔画太细的字体渲染出来字符会发虚。生成底图之后还有一步很关键把车牌贴到自然背景中。背景可以是网上下载的街道、停车场照片也可以是纯色噪声图。贴图时随机位置、随机缩放、随机旋转必要时加一点透视变换和高斯模糊让检测器不至于靠「边缘太整齐」这个假特征来认车牌。这一步做得越充分后面真实场景上翻车的概率越低。如果你只想快速验证流程可以先只用几张背景图跑通后续再扩充。3.2 batch_resize_images.py 和 create_annotations.py统一尺寸与生成 YOLO 标注生成完带车牌的合成图后下一步是统一尺寸。YOLOv8 训练时会把输入 resize 到固定尺寸一般是 640x640但训练集里如果混着 1920x1080 和 800x600 两种原图数据加载和标注换算都会出问题。batch_resize_images.py 就是把这一批合成图统一缩放到同一规格同时保持车牌相对位置不偏。# batch_resize_images.py 关键逻辑按常见实现还原 import os from PIL import Image def batch_resize(src_dir, dst_dir, target_size(640, 640)): os.makedirs(dst_dir, exist_okTrue) for name in os.listdir(src_dir): if not name.lower().endswith((.jpg, .png, .jpeg)): continue img Image.open(os.path.join(src_dir, name)) img img.resize(target_size, Image.LANCZOS) # 高质量缩放 img.save(os.path.join(dst_dir, name))这里用 LANCZOS 重采样而不是默认的 BICUBIC是为了在缩小图片时保留更多边缘信息车牌字符的笔画在缩放后不至于糊成一团。target_size 要和后面 YOLOv8 训练配置里的 imgsz 保持一致如果训练时打算用 640这里就缩到 640省得训练时再二次 resize 引入误差。另外注意如果原图不是 640x640 的比例直接 resize 会把图压变形常见做法是先 letterbox 再缩放或者生成时就用统一比例的背景图这个项目里用的是后者所以脚本能直接跑。create_annotations.py 负责生成标注。YOLO 格式的标注是每个目标一行class_id center_x center_y width height其中四个坐标值都是相对图片宽高的比例值范围 0 到 1。脚本在生成车牌图时就已经记录了车牌的原始坐标这里只需要换算即可# create_annotations.py 关键逻辑按常见实现还原 def convert_to_yolo(plate_bbox, img_w, img_h): x1, y1, x2, y2 plate_bbox # 像素坐标 cx ((x1 x2) / 2) / img_w cy ((y1 y2) / 2) / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h return f0 {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}一个容易忽略的坑是坐标是相对比例而不是像素值所以 batch_resize 之后的图必须保证只有尺寸变了、内容没有裁剪否则之前记录的像素坐标换算后就会错位。所以这两条脚本的执行顺序不能乱先 resize再基于 resize 后的尺寸生成标注。如果你打算改成自己拍的真实车牌图需要额外标注工具但标注文件格式是一样的YOLOv8 直接兼容。我一般会生成完之后随机挑几张把标注框画回图上肉眼看一眼确认框没有整体偏移再进训练。3.3 split_dataset.py把数据集切分到 images 和 labels 双目录YOLOv8 训练时需要 images/train、images/val、labels/train、labels/val 四件套。split_dataset.py 的作用是把合成图和对应的 txt 标注按比例切开同时保证同一张车牌的图片和标注不会分开。# split_dataset.py 关键逻辑按常见实现还原 import os, random, shutil def split(src_images, src_labels, dst_root, val_ratio0.2): names [f[:-4] for f in os.listdir(src_images) if f.endswith(.jpg)] random.shuffle(names) split_idx int(len(names) * (1 - val_ratio)) for phase in [train, val]: for sub in [images, labels]: os.makedirs(os.path.join(dst_root, sub, phase), exist_okTrue) for i, name in enumerate(names): phase train if i split_idx else val shutil.copy(f{src_images}/{name}.jpg, f{dst_root}/images/{phase}/{name}.jpg) shutil.copy(f{src_labels}/{name}.txt, f{dst_root}/labels/{phase}/{name}.txt)val_ratio 默认 0.2即 80% 训练、20% 验证。对于仿真数据这个比例够用但要注意必须提前 shuffle否则脚本按文件名顺序切分时前面可能全是同一种底色或同一批背景的图验证集会失去代表性。这一步做完YOLOv8 训练需要的数据结构就齐了。如果你想让验证集更接近真实场景可以单独准备一小批不同的图片放进去手动复制到 val 目录里替换同名文件这样训练时就能看到真实数据的 mAP避免「仿真集高分、真图全挂」的尴尬。提示如果你用的是项目里自带的生成脚本建议先跑一遍小批量比如 50 张确认标注框位置正确再大批量生成。花两分钟看几张标注图能省下后面训练跑偏的几小时。4. 双模型训练YOLOv8 定位车牌 LPRNet 读字符的参数与流程4.1 训练 YOLOv8 检测器配置文件、超参数与损失曲线YOLOv8 训练自定义数据集是这套流程里最标准的一步。数据准备好了之后需要先写一个 data.yaml 文件指定训练/验证图片目录和类别数。这个项目里检测目标只有一类就是车牌所以 names 只需要一行。yolov8 环境配置本身不算复杂但要注意版本ultralytics 的接口在 8.x 演进过几轮命令行的主参数名基本稳定但如果你用的源码版本比较老个别参数可能需要对照 README。项目里通常把训练命令写在 runs 目录对应的脚本或 README 中常见做法是直接命令行调用# 使用 ultralytics 训练 YOLOv8 检测器 yolo detect train dataconfig/plate.yaml \ modelyolov8n.yaml \ epochs100 \ imgsz640 \ batch16 \ projectruns \ nameplate_detect几个参数值得展开。modelyolov8n.yaml 是选 nano 版本网络最小、训练最快对于「只要检测车牌一个类」的任务来说nano 的容量完全够而且后面部署到 CPU 或者边缘设备时速度优势明显。imgsz640 是 YOLOv8 默认输入尺寸和第 3 章的 resize 目标保持一致。batch16 取决于显存8G 显存跑 nano 开 16 没问题如果训练时报 CUDA out of memory降到 8 或 4 就行。epochs 个人建议起步 100仿真数据比较简单100 轮足够看到验证集 mAP 收敛平台。训练过程中建议盯两个东西。一是 runs/plate_detect/weights/best.pt 和 last.pt 是否正常生成best.pt 是验证集表现最好的权重后面部署用它二是 runs/plate_detect/results.csv 里的验证集损失曲线如果 loss 一路震荡不下降先怀疑标注数据错了再怀疑学习率过大。现在版本里也可以用yolo detect train带plotsTrue直接产出曲线图方便最后写报告时贴图。如果想画损失函数曲线图更精细也可以直接读 results.csv 用 matplotlib 自己画比截图清晰得多。4.2 训练 LPRNet 识别器main.py 里的字符表、学习率与数据加载YOLOv8 解决了「车牌在哪」LPRNet 解决「车牌是什么」。它是个轻量 CNN 加 RNN 结构的序列识别网络输入是固定尺寸的车牌小图输出是字符序列的概率分布。LPRNet 训练用的数据不是整图而是只有车牌的裁剪图所以第 3 章生成的仿真车牌底图在这里直接复用。训练入口在 LPRNet/main.py。里面最重要的两个配置是字符表和输入尺寸。字符表决定了网络能输出哪些字符必须和 generate_lpr_data.py 里的字符集完全一致否则模型会输出训练时没见过的字符。输入尺寸一般取宽 94、高 24这是 LPRNet 论文里的经典参数对应国内车牌的比例。# LPRNet/main.py 关键部分按常见实现还原 from torch.utils.data import DataLoader from dataset import LPRDataset from model import LPRNet CHARS 京津沪渝冀豫云辽黑湘皖新蒙琼川贵桂甘陕青藏吉宁苏浙赣鄂闽粤 \ ABCDEFGHJKLMNPQRSTUVWXYZ0123456789 lprnet LPRNet(class_numlen(CHARS)) train_loader DataLoader( LPRDataset(images_lpr/train, CHARS), batch_size64, shuffleTrue, num_workers4 ) optimizer torch.optim.Adam(lprnet.parameters(), lr0.001)这里 batch_size64 是因为 LPRNet 本身很轻单张图只有 94x2464 的 batch 在普通显卡上毫无压力。学习率 0.001 是 Adam 的常见默认值训练时如果 loss 不降可以先试 0.0005如果 loss 骤降后立刻反弹则是学习率偏大。num_workers4 表示用 4 个子进程加载数据Windows 下如果报多进程错误改成 0 用主进程加载最稳。数据集目录 images_lpr 里的图片要提前统一成 94x24这可以直接复用第 3 章的 batch_resize 脚本只是把 target_size 改成 (94, 24)。LPRNet 的 loss 用的是 CTC Loss因为它不要求输出和真实字符序列严格等长对齐网络自己决定每个时间步输出什么字符训练时只需要给真实车牌字符串即可。这也是 LPRNet 比传统先分割字符再识别的方式省事的地方。主训练循环里通常还会接一个验证集上的准确率计算注意这个准确率是「整串完全匹配」才算对所以早期可能一直是 0不用慌等 loss 降到一定程度再关注准确率。4.3 权重导出与 output_lpr 目录的组织方式训练完成后LPRNet 会保存一个权重文件到 model/output_lpr 目录输入是车牌小图输出是字符序列。这里依赖一个解码过程网络输出的是一串概率分布需要按时间步取 argmax再去掉重复字符和空白符才能拼成最终车牌号。很多人在这一步翻车不是模型没训好而是解码逻辑没配对。# 解码关键逻辑CTC 贪心解码 import torch def decode_lprnet(logits): # logits: [batch, seq_len, num_classes] preds logits.argmax(dim-1) # 每个时间步取概率最大的字符索引 result [] prev -1 for idx in preds[0].tolist(): if idx ! prev and idx ! len(CHARS): # 去掉重复和空白符 result.append(CHARS[idx]) prev idx return .join(result)注意 idx ! prev 这个条件它处理的是相邻重复字符。比如车牌里有两个连续相同的字母「AA」CTC 解码不能直接把重复的都去掉要靠空白符区分这里用贪心解码虽然简单但对于课程设计场景够用。如果发现识别的车牌总是少一位或多一位优先检查解码逻辑里空白符和重复字符的处理而不是重训模型。output_lpr 目录里通常保存着 best 权重和一个最近一次的推理输出Flask 端加载的就是这个权重。跑通训练后记得把 LPRNet 权重路径和字符表写进 app.py 的配置里两边字符表不一致是部署期最常见的错位问题。权重导出时如果发现 .pth 文件里只有网络参数没有字符表元信息建议在 app.py 里写死一份 CHARS别从权重文件里猜。5. 常见问题避坑训练和部署容易翻车的五个细节5.1 现象YOLOv8 训练完runs 里没有 best.pt训练日志显示正常结束但 runs/plate_detect/weights 下只有 last.pt没有 best.pt。原因是训练过程中验证集的评价指标一直没有超过初始值版本逻辑上可以只保留 last。解决方法是看 results.csv 里的 mAP50 数值如果是 0 或者接近 0问题几乎都在标注上检查 labels 里的 txt 是不是空文件、坐标有没有超过 1 或者为负数。另外确认 data.yaml 里的路径写的是绝对路径还是相对路径YOLOv8 对相对路径的解析比较严格建议直接写绝对路径。5.2 现象仿真数据训练后在真实车牌图上检测不到训练时 mAP 很高一换真实照片就框不出来。原因是仿真数据太「干净」车牌位置固定、背景单一、没有光照变化和遮挡检测器学到的是仿真图本身的特征而不是车牌的通用特征。解决方向有三个一是生成脚本里加大旋转、透视、模糊和亮度变化的范围二是往背景图库里加入真实街道照片提升背景多样性三是用小批量真实车牌图做微调。血泪经验是仿真数据只能当 baseline想上真实场景必须混真实数据微调这个比例至少要到 3:1 甚至 1:1。5.3 现象LPRNet 识别结果乱码输出里有 / 或者空串decode 出来的字符串包含乱码字符或者长度不对。先查字符表LPRNet 输出的索引是字符表里的位置app.py 里解码用的字符表和训练 main.py 里定义的 CHARS 必须逐字一致多一个少一个都会错位。另一个常见原因是输入尺寸Flask 端把裁剪出的车牌图直接送进模型没有 resize 到 94x24导致张量尺寸不匹配或者数据加载时自动做了不明缩放特征就被破坏。解决方法是固定一个预处理函数统一 resize、转灰度、归一化三步训练和推理用同一套。我在本地调试时踩过这个坑训练和推理一个用了 RGB 一个直接读灰度输出直接乱套。5.4 现象Flask 能启动但上传图片后接口一直转圈先看后端控制台最常见的错误是端口被占或 uploads 目录不存在导致保存图片时报错。Flask 默认 5000 端口如果之前跑过其他服务先换端口app.run(host0.0.0.0, port5001)。另一个隐蔽问题是 Vue 前端配置的代理地址写死成 localhost而后端跑在别的机器或 Docker 里联调时改成实际的 IP。检查顺序是先 curl 后端接口确认返回 JSON再查前端 network 面板里真实请求打到了哪里。注意 vite 的代理配置在 vite.config.js 里改了之后需要重启 dev server 才生效。5.5 现象推理时 CPU 跑得很慢一张图要好几秒YOLOv8n 在 CPU 上推理一张 640 的图一般几百毫秒如果到几秒级先确认是不是误加载了大模型版本比如把 yolov8x 的权重当成了 nano。另外检查 app.py 里是否每次请求都重新加载了一遍模型权重正确做法是在进程启动时加载一次推理时复用。LPRNet 本身很轻耗时高的话多半是图像预处理里有重度操作比如大尺寸原图反复缩放可以在上传时先压缩到合理分辨率比如长边压到 1280 以内既不影响检测精度又能明显提速。6. 全链路验证与联调用一张真实照片走通上传、检测、识别、回显整套系统验证不复杂先把后端起起来python app.py后端起来后访问 Vue 前端上传一张包含清晰车牌的照片。观察几个关键节点前端 uploads 目录里是否多了一张新图后端控制台是否打印了检测框坐标和识别结果前端页面上车牌号码和位置框是否正常显示。这里有个小技巧把 YOLOv8 检测结果的置信度阈值调低到 0.3会在前端多显示几个候选框用来观察检测器到底把哪里误判成了车牌方便判断是模型问题还是预处理问题。如果是绿牌新能源车还要确认生成训练数据时包含绿底类别否则检测框大概率框不准。如果识别结果错了可以先在离线环境单独验证 LPRNetpython main.py --test model/output_lpr/best.pth test_images/输出结果会逐张打印预测车牌和真实车牌。对于真实车牌识别错的样本常见原因不是模型坏了而是检测框裁歪了字符贴边或者缺了一角。所以排查顺序永远是先看框再看字框错了识别器不可能对。我还习惯在 app.py 里加一行日志把每次请求的检测框坐标、置信度和识别结果一起写到本地文件这样后面整理答辩材料、复现翻车现场都有据可查。在这套项目上我最终把检测置信度阈值、LPRNet 字符表、输入尺寸三个常量从代码里抽了出来放到 config 文件里从那以后每次跑都强制走一遍「上传 → 看框 → 看字 → 查日志」四步。别嫌这一步慢课程设计答辩现场被问到「你这个模型为什么把广告牌上的字也框出来」的时候你手里有日志和置信度阈值能现场调比重新训模型体面得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表