ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

YOLOv8+LPRNet车牌识别:从检测到字符序列的完整落地指南

YOLOv8+LPRNet车牌识别:从检测到字符序列的完整落地指南 简介面向毕业设计的YOLOv8LPRNet车牌识别源码包专为计算机视觉、图像处理相关专业的学生打造适用于课程设计、毕业设计以及车牌识别方向的技术入门。资源以Python为主要编程语言整合了目标检测与光学字符识别两大模块能够完成从车牌定位、字符分割到车牌号码输出的完整流程。整套资源共840个文件压缩后约646MB。其中包含260张图片样本可作为训练与测试数据164个Python脚本覆盖数据加载、模型构建、训练与推理等环节69个pth、32个pt和3个onnx文件为不同格式的模型权重与导出模型便于直接加载使用71个yaml文件用于配置模型结构和训练参数另有csv、xml等标注或结果文件帮助复现实验。已有492人学习下载。资源附带TensorBoard训练日志可以直观分析训练过程中的损失与精度变化便于调参优化同时提供了完整的工程化代码结构读者可直接基于现有权重进行推理也可重新训练自己的车牌数据集是毕业设计实践和质量较高的参考项目。1. 毕业设计里最稳的车牌识别方案就是 YOLOv8LPRNet毕业设计里最常见的车牌识别方案基本就是 YOLOv8 做检测 LPRNet 做识别。打开那份源码.zip你会发现真正的工作量不在模型本身而在两件事一是把检测框稳定地对应到车牌字符串二是让整条链路在 CPU 或低算力设备上也能跑出可看的帧率。这篇文章按我处理类似项目时的顺序来讲先把 YOLOv8 和 LPRNet 的职责边界切开再给出一套直接能跑的推理脚本和配置然后说到训练自己的车牌数据集时要调哪些参数、坑在哪。整套思路适合拿来做毕业设计也适合刚接触车牌识别的后端或算法工程师快速落地。把检测和识别当两个独立模型串起来会少走很多弯路。2. 先拆任务YOLOv8 做检测、LPRNet 做识别真正的边界在车牌区域2.1 YOLOv8 的检测能力边界C2f 与多尺度输出车牌识别不是把一张图直接扔给某一个网络而是先定位“车牌在哪”再把定位到的局部图转成字符。YOLOv8 在这一步负责的是目标检测它输出的是一组带类别和置信度的矩形框。和传统 HOG SVM 滑窗比它的优势是多尺度特征融合能同时兼顾大车近景和远处的小车牌和单阶段分割网络比它少一个像素级标注成本毕业设计阶段更容易控制工作量。YOLOv8 的 backbone 用了 C2f 模块这个结构把上一层特征分别经过多个 Bottleneck 后再拼接相当于在保持通道数的情况下增加了梯度回传路径。对于 40×40 像素以下的小车牌C2f 在浅层输出的 80×80 特征图上能保留更多空间信息。这个设计让 YOLOv8n 这种轻量版也具备不错的小目标召回能力不必一上来就选参数量更大的模型。YOLOv8 相比上一个版本还有一个明显变化检测头从 anchor-based 换成了 anchor-free。每个位置直接回归到目标的四个边少了对预置 anchor 尺寸的依赖。这对车牌识别是个好消息因为不同车牌的长宽比在各类数据集中相对集中anchor-free 用基于关键点的分配策略在长宽比偏离均值时不容易丢目标。配合imgsz640即使车牌斜着出现在画面边缘也能给出稳定框。模型尺寸方面YOLOv8 官方给了一套从 n 到 x 的尺度。在车牌识别链路里我的优先级经常是yolov8n优先于yolov8s因为检测框只是给 LPRNet 提供裁剪区域边界差几个像素对最终字符结果影响不大。表 1 是三个常用变体的参考差异注意这里的延迟是在 T4 GPU 上的相对值实际 CPU 部署会放大 5 到 10 倍。模型变体参数量约百万输入分辨率相对延迟适合场景YOLOv8n3.2640×640低CPU/边缘设备优先帧率YOLOv8s11.2640×640中GPU 服务器均衡YOLOv8m25.9640×640高夜间/远距离小目标多的情况实际上车牌检测单类目标难度比 COCO 80 类低训练充分的yolov8n在 1080p 监控流上就能提供足够的召回率。选型时不必盲目往上走。2.2 LPRNet 为什么用序列识别而不是字符分割拿到裁剪后的车牌图之后常见还有两条路一条是把字符逐个切出来再单独分类另一条是像 LPRNet 这样直接输出整个字符串。逐个切割的流程看似直白但车牌背景有渐变、反光、铆钉和阴影字符阈值化的鲁棒性很差一个粘连就会连锁错误。LPRNet 绕开字符级标注通过循环网络把宽度方向按时间展开每个时间步预测一个字符或空白最终用 CTC 对齐。LPRNet 的输入一般是 94×24 的单通道或三通道图像。经过 CNN 后高度方向被压缩到 1宽度方向保留为多个时间步。RNN 部分使用两层双向 LSTM 或 GRU它学习的是字符之间的上下文依赖关系比如京A后面更可能出现数字而不是字母的规律。最后接一个全连接层输出形状是(时间步数, 字符集大小)。推理时取每个时间步上概率最大的索引再用 CTC 贪心解码把重复项和 blank 合并。2.2.1 字符集设计和 CTC 的blank位LPRNet 的字符集是训练前就要锁死的。国内车牌通常包括省份汉字、英文字母和数字。常见字符集会写成一个 list例如[京, 津, 沪, ..., A, B, ..., 0, ..., 9]然后在末尾追加一个blank符号。这个 blank 在 CTC 中表示当前位置没有有效输出解码时碰到 blank 直接跳过。这里有一个高频出错点CTC 合并规则要求连续相同字符之间必须有 blank 隔开否则真实字符串中的连续重复字符会被合并掉。比如京A12234中两个连续2会在预测路径里表达为2 blank 2解码时先去掉 blank 再合并重复才能得到正确的22。源码里如果出现字符串长度对不上可以优先查这个解码逻辑。有人会问既然 YOLOv8 检测和 LPRNet 识别是两个独立的网络能不能把两者拼起来端到端微调技术上可以用 RoIAlign 之类的算子把检测结果变成可微张量再反传梯度但车牌字符有其特殊的序贯先验检测和识别任务的学习节奏差别很大。联调容易让检测部分的梯度主导整个 loss反而丢掉了 CTC 在序列对齐上的优势。所以毕业设计场景下我更推荐保持两阶段各自训练后再用启发式规则做结果融合。拆分到这里整个系统的边界就很清楚了检测负责空间上的定位识别负责序列上的翻译。两者可以独立训练、独立测试也可以组合调优。这一步想清楚之后再去看 zip 里的代码结构就不会陷入一堆文件夹里。3. 环境配置与最小源码跑通从 zip 到第一张车牌3.1 依赖版本怎么选GPU 是不是必须从 zip 里复现项目最重要的是一开始就把环境锁住。Python 版本建议 3.8 到 3.10太高容易遇到 CUDA 扩展编译问题太低则装不上新版 ultralytics。PyTorch 选 1.13 到 2.1 都行如果源码用了旧式torch.nn.DataParallel2.x 依然兼容。LPRNet 依赖于 torch 的 RNN 模块没有额外的 CUDA 算子纯 PyTorch 就能训练。是否必须用 GPU 要看用途。跑离线视频识别CPU 完全够。用yolov8n在 CPU 上做单帧 640 分辨率检测约 80 到 120 毫秒再加上 LPRNet 的 5 到 8 毫秒整体在 10 FPS 左右。训练阶段强烈建议有 GPU否则 YOLOv8 几百轮训练在 CPU 上要跑十几个小时。常见的 GTX 1660 Ti 显存 6Gbatch size 设在 8 到 16 之间即可。# 在项目根目录执行建议先建虚拟环境 python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install torch2.1.0 torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics opencv-python numpy # 如果源码里有 requirements.txt先看版本再装 pip install -r requirements.txt这里--index-url只作用到 torch 和 torchvision限制为 CUDA 11.8 的预编译包。如果你只有 CPU把这一行换成普通pip install torch torchvision就行。装完之后用python -c import torch; print(torch.cuda.is_available())确认设备状态底下的推理脚本会根据返回值自动选择设备不要硬编码cuda。3.2 最小推理脚本检测框、裁剪、识别三步下面是串起两个模型的最小脚本。它假设 zip 里有weights/yolov8n-lp.pt和weights/lprnet.pth以及 LPRNet 的网络定义文件。代码里最需要注意的是load_state_dict之后必须切到eval()模式否则 BatchNorm 和 dropout 在推理时行为不一致识别结果会忽好忽坏。from ultralytics import YOLO import cv2 import torch # 初始化两个模型 detector YOLO(weights/yolov8n-lp.pt) # 车牌检测权重 lprnet LPRNet(config/lprnet.py) # 自定义网络结构 state torch.load(weights/lprnet.pth, map_locationcpu) if state_dict in state: state state[state_dict] lprnet.load_state_dict({k.replace(module., ): v for k, v in state.items()}) lprnet.eval() def recognize_plate(frame, devicecuda if torch.cuda.is_available() else cpu): # 1. 检测车牌区域 results detector(frame, conf0.35, iou0.6, imgsz640, verboseFalse) boxes results[0].boxes.xyxy.cpu().numpy().astype(int) plates [] for x1, y1, x2, y2 in boxes: # 2. 向外扩 10%避免字体顶到边界 h, w frame.shape[:2] pad_w int((x2 - x1) * 0.1) pad_h int((y2 - y1) * 0.1) x1 max(0, x1 - pad_w); y1 max(0, y1 - pad_h) x2 min(w, x2 pad_w); y2 min(h, y2 pad_h) crop frame[y1:y2, x1:x2] crop cv2.resize(crop, (94, 24)) # 统一成 LPRNet 输入尺寸 img torch.from_numpy(crop.transpose(2, 0, 1)).float() / 255.0 img img.unsqueeze(0).to(device) # 3. 识别序列 with torch.no_grad(): logits lprnet(img) # [1, num_class, time] pred logits.argmax(dim1).squeeze().cpu().numpy() text ctc_decode(pred) # 合并重复并去空白 plates.append(([x1, y1, x2, y2], text)) return platesconf0.35表示检测框得分低于这个值的直接丢掉。车牌检测比通用目标检测简单这个阈值可以放得比较开路人身上类似车牌纹理的误检通常靠 IoU 和后续识别阶段的置信度过滤。iou0.6是 NMS 的参数监控画面里不会出现多个车牌重叠的情况默认值即可。LPRNet 输出的维度是[1, num_class, time_steps]而不是[1, time_steps, num_class]。这是因为 PyTorch 的 CTCLoss 希望在最后维度放字符集概率。如果继续沿用batch_first的思路把第三维当成字符集argmax会取到一个无关维度解码结果变成乱码。所以代码里pred logits.argmax(dim1)的dim1是 num_class取完再做一下squeeze得到[time_steps]。ctc_decode实现里有两个关键点先合并连续重复的索引再删除blank。如果只合并重复不删 blank会把预测结果里的-1或blank索引映射到字符集尾部出现莫名其妙的字符。下面是其中一种实现def ctc_decode(pred_seq, blank_index): result [] prev -1 for idx in pred_seq: if idx ! prev and idx ! blank_index: result.append(char_list[idx]) prev idx return .join(result)这段代码没有用torch.unique因为 unique 会破坏可重复字符的规则手动遍历才能保留连续空白分隔后的真实重复字符。字符集的顺序加载自config/char_dict.jsonblank_index一般等于len(char_list)也就是一开始多预留的一个字符。3.3 常见运行时报错shape 不匹配和权重缺失跑这个脚本最常看到两类报错。一类是size mismatch for rnn.weight_ih_l0说明下载的 LPRNet 权重和你代码里的网络结构不一致。常见原因有两个输入尺寸不同导致 RNN 的输入特征维度变化或者对方训练时用了DataParallel权重 key 前面带module.前缀。去掉前缀的方法上面代码里已经写了。另一类是unkown argument name: device多见于把模型迁移到 MPS 或 CPU 时误传了参数。建议把设备判断收敛在入口处模型和输入使用同一device不要让模型在 GPU、输入在 CPU。最简单的方式是只调用一次to(device)后续所有Tensor.to(device)都复用同一个变量。跑通这一步离线画面上能看到来自 LPRNet 的字符输出说明链路本身没毛病。接下来的问题是如何拿自己的数据把两个模型再训练一轮让识别更贴合拍摄环境。4. 训练自己的数据集与参数调优把 YOLOv8LPRNet 调出稳定识别率4.1 车牌检测数据集组织YOLO 格式和 yaml 配置文件要用自己的车牌数据训练 YOLOv8第一步是统一标注格式。labelImg 或 X-AnyLabeling 导出 YOLO 格式后每个 txt 文件名与图片名对应内容为class x_center y_center width height全部归一化到 0 到 1。车牌检测只做单类所以 class 固定为 0。目录结构按 ultralytics 的要求组织好data/lp_det/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── lp.yaml其中lp.yaml写训练路径和类别数path: data/lp_det train: images/train val: images/val nc: 1 names: [plate]训练命令用 CLI 一行完成yolo detect train \ datalp.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ device0 \ patience20从预训练的yolov8n.pt微调比从随机权重从头训练收敛快得多。车牌虽然是新类别但 COCO 预训练特征里的边缘、纹理提取能力可以迁移100 轮基本够。imgsz不建议降到 320因为车牌属于小目标降低输入分辨率会直接砍掉浅层高分辨率特征图上的空间信息。4.2 检测侧增强与训练参数调整YOLOv8 的 mosaic 增强默认开启。它的作用是让模型见过更多遮挡组合但对背景单一的车牌场景mosaic 也可能让车牌的相对尺寸变成不自然的小块导致验证集上 mAP 和实际表现脱节。一个常见 trick 是训练后半段关闭 mosaicyolo detect train ... close_mosaic30close_mosaic30表示训练 30 轮之后关闭 mosaic只使用普通增强让模型在最后阶段微调到真实分布。对应的hsv_h、hsv_s可以适当提高因为真实监控相机会在一天内经历明显的色温变化。如果发现检测框在视频里抖动得很凶不要急着一味提高conf先看检测框的宽高比分布。车牌宽高比一般在 3:1 到 5:1 之间如果检测框经常出现接近正方形的输出可能是训练数据里有大量近距离车头照片把车灯或散热器格栅也框进去了。此时可以在后处理里加一个ratio_range过滤比单纯提高阈值有效。夜间车牌识别难的主要原因不是模型而是暗光下 YOLOv8 检测框的不稳定。摄像头自动曝光会把整帧调亮导致车牌区域过曝、边框和字符融为一体。建议训练 YOLOv8 时不只用原始图像而是额外生成一组低曝光高斯噪声的增强图。具体做法是在数据加载阶段随机把图像亮度乘 0.6 到 0.8再叠加 sigma 为 5 到 10 的噪声。训练时还可以通过照片的 EXIF 信息对比不同分辨率下的识别效果。如果你发现 1600 宽的大图检测正常但手机抓拍 720p 的图经常漏检不要只调算法先确认 LPRNet 的输入裁剪插值方式。cv2.INTER_AREA在缩小远处小图时保留更多细节INTER_LINEAR适合正常尺寸的放大统一改成INTER_AREA会让整个链路在低分辨率输入下更稳。4.3 LPRNet 训练合成数据加少量真实数据LPRNet 不像 YOLOv8 有预训练权重可以直接下冷启动训练效果更容易受数据影响。常见做法是先用合成数据预训练再用真实数据微调。合成车牌可以用 Pillow 或 OpenCV 画文字、微调字体颜色、加随机背景批量生成 5 万张左右。合成数据能保证字符序列和背景完全可控但缺少真实镜头下的模糊、反光所以微调阶段至少保留 1000 到 3000 张现场拍的照片。LPRNet 训练脚本中需要关注三个超参batch_size、lr和milestones。一个常用配置表如下超参数常见值说明batch_size32~128偏大更稳RNN 时间步会让显存涨得比普通 CNN 快lr 初始3e-4大于 1e-3 容易 loss 震荡milestones[15, 25]30 轮内分两次降学习率输入尺寸24×94高 24宽 94不能随意对调blank label字符集长度字符集 list 的最后一位训练时 loss 用torch.nn.CTCLoss它要求预测序列维度是(T, N, C)这里T是时间步数N是 batchC是字符集大小。很多人会在这里报维度错误因为torch.nn.LSTM在 batch_first 模式下输出形状为(N, T, C)需要transpose(0, 1)后再喂给 CTCLoss。这个转置在源码里特别容易被忽略。criterion torch.nn.CTCLoss(blankblank_index, zero_infinityTrue) # logits: [T, N, C], targets: [N, target_len] loss criterion(logits, targets, input_lengths, target_lengths)zero_infinityTrue可以在出现 log 0 时跳过这些样本避免某个空白字符概率为 0 导致 loss 变成 NaN。车牌字符短input_lengths全部初始化为时间步数即可target_lengths用每个字符串的实际长度。4.4 联调顺序先测组件指标再测端到端把训练好的两个模型拼起来之前最好分别确认指标。单独跑 YOLOv8在验证集上统计检测框 IoU 0.5 的召回率单独跑 LPRNet在裁剪图上算字符准确率和整串准确率。如果组件的准确率双双超过 90%端到端瓶颈往往出现在两个模型传递的接口上比如检测框被裁剪得过于靠边、或者路面阴影导致 LPRNet 输入对比度偏低。调整顺序可以按「先检测后识别、先在线后离线」推进。检测召回率不够就去加大 yolo 输入分辨率、调低conf检测够了但识别差就去检查裁剪边框是否包含完整车牌、LPRNet 是否用自己的数据集微调。联调阶段用测试视频打点统计不要只看静态图片指标因为你最终要的是一个连续视频流中的可用率。5. 进阶把两阶段识别变成带时序验证的可用模型5.1 用概率累加做多帧投票过滤随机错字实时视频流里单帧识别偶尔错一个字符是很常见的。直接在输出字符串上做多帧等值投票看起来简单但会漏掉区间内真实出现的车牌变化。更可靠的做法是在字符概率层面做累加。因为 LPRNet 推理输出维度是[1, num_classes, time_steps]先把不同帧的 softmax 概率逐时间步累加再做argmax相当于把置信度也纳入投票。import numpy as np prob_accum np.zeros((max_time_steps, num_classes)) for frame in frame_buffer: prob_accum torch.softmax(logits, dim1).squeeze().cpu().numpy().T result_seq prob_accum.argmax(axis1)这里prob_accum的形状是(time_steps, num_classes)和 LPRNet 输出转置后的形状一致。累加前不用归一化因为每帧概率和近似为 1五帧累加后的量纲是 5不影响比较。如果要进一步抑制抖动可以给新帧更高的权重比如prob_accum 0.7 * prob_accum 0.3 * current_probs。5.2 验证阶段不要只报一个准确率毕业设计和真实项目都需要一套能解释的指标。建议至少分开报告检测召回率、字符准确率Correct Character Rate和整串准确率。检测召回率定义为 IoU 0.5 的车牌被检测出的比例字符准确率在裁剪图数据集上单独评估整串准确率才反映端到端效果。如果字符准确率高但整串准确率低说明偶尔错一两个字符占主导需要增强字符级数据而不是重新训练。部署阶段如果预算紧张可以优先试 CPU 上的 ONNX Runtime。YOLOv8 和 LPRNet 分别导出 ONNX再在同一个进程里串联。导出 YOLOv8 用yolo export modelbest.pt formatonnxLPRNet 则用torch.onnx.export指定动态 batch 维度方便一次识别多张裁剪图。比如 LPRNet 的导出片段torch.onnx.export( lprnet, dummy_input, lprnet.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}}, opset_version12 )opset_version12对 GPU 和 CPU 都比较稳妥太高可能出现算子兼容问题。低功耗边缘设备上跑 LPRNet 还可以做 INT8 量化。由于 LPRNet 的 RNN 部分对数值精度比较敏感建议只量化 CNN backboneRNN 保持 FP16 或 FP32。用 ONNX Runtime 的quantize_dynamic可以只量化全连接和卷积层动态量化后模型体积通常缩减到原来的四分之一字符准确率只损失 0.5 到 1 个百分点。导出后多帧推理、字符缓冲、NMS 过滤都放在服务端完成输入输出图像尺寸保持一致便于接口层统一。整个链路调完后至少要把测试视频换成夜间、逆光、雨天各测一遍再往下谈部署指标。本文还有配套的精品资源点击获取
返回列表