首页 / 资讯中心 / 文章详情

YOLOv11+LPRNet车牌识别源码拆包:从环境配置到ONNX部署全流程

YOLOv11+LPRNet车牌识别源码拆包:从环境配置到ONNX部署全流程 ★ FEATURED ARTICLE
简介本资源为基于YOLOv11与LPRNet的车牌识别系统完整项目包面向计算机相关专业做毕业设计、课程设计或期末大作业的学生以及需要深度学习项目实战练习的学习者。项目经导师指导并获评审99分代码完整可运行适合零基础小白按目录逐步复现。压缩包共209个文件约148.26MB包含23个Python源码、22个Jupyter Notebook实验记录、16个pth与4个pt模型权重、1个onnx导出模型以及90张png、27张jpg数据集图像和yaml配置、csv训练日志等覆盖数据标注、模型训练、推理部署全流程。已有185人学习下载。读者可获得YOLOv11车牌检测与LPRNet字符识别的双阶段方案、可复现的训练脚本与权重文件、完整数据集及实验笔记便于快速搭建环境、理解检测与识别衔接逻辑并在此基础上完成论文撰写与答辩演示。1. 车牌识别系统源码拆包YOLOv11 加 LPRNet 这套组合到底能不能直接跑车牌识别这个方向每年毕业设计季都会被翻出来做一遍。原因很实在它同时踩中了目标检测和字符识别两个深度学习核心任务工作量够、答辩好讲、演示效果直观。但真正动手做过的人都知道坑不在模型本身而在两段式流程的衔接上——检测框裁得不准识别再强也白搭字符对齐没处理好CTC 解码出来就是一堆重复字。这套资源给的是 YOLOv11 LPRNet 的完整 Python 实现包含源码、训练好的模型文件和数据集。YOLOv11 负责从整张图里定位车牌区域LPRNet 负责对裁出来的车牌做端到端字符识别。适合正在做计算机毕业设计、需要一套能跑通且结构清晰的车牌识别方案的人。下面按实际拆包和复现的顺序把这份资源从环境配置到推理验证完整走一遍。2. 环境配置与依赖安装从 python 安装到 vscode python 环境配置2.1 为什么选 YOLOv11 而不是 v8 或 v10YOLOv11 在骨干网络里引入了 C3k2 模块把部分卷积替换成了更轻量的跨阶段部分连接结构同时检测头换成了深度可分离卷积。对车牌检测这个任务来说最直接的收益是小目标召回率有提升——车牌在整张图里通常只占很小一块区域尤其是远距离抓拍场景。YOLOv11 的 neck 部分用了 PAN 结构做多尺度特征融合P3 层的高分辨率特征保留得比 v8 更完整这对小尺寸车牌的定位帮助明显。LPRNet 这边则是另一条技术路线。它不走传统的「分割字符 逐字识别」流程而是用 CNN CTC 直接做序列预测。好处是不需要字符分割标注一张车牌图片进去直接输出字符序列。网络结构很轻参数量不到 2M推理速度在 CPU 上都能接受。和 YOLOv11 搭配整个系统对硬件的要求就降下来了普通笔记本没有独显也能跑推理。2.2 环境搭建的具体步骤先确认 Python 版本。这套源码我实测在 Python 3.8 到 3.10 之间都能跑推荐 3.9兼容性最稳。Python 安装时记得勾选「Add Python to PATH」不然后面 pip 命令会找不到。# 创建虚拟环境避免和系统 Python 冲突 python -m venv plate_env # Windows 激活 plate_env\Scripts\activate # Linux / macOS 激活 source plate_env/bin/activate虚拟环境建好之后装依赖。源码包里一般会带 requirements.txt但根据我的经验直接 pip install -r 经常会卡在 torch 的版本上。建议先手动装 torch再装其余依赖。# 先装 PyTorch根据是否有 CUDA 选择版本 # 有 NVIDIA 显卡且装了 CUDA 11.8 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 没有独显装 CPU 版本 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 再装其他依赖 pip install opencv-python numpy pillow pyyaml tqdm这里有个参数要注意--index-url指定的是 PyTorch 官方轮子源不走这个源的话 pip 会去默认源找国内网络环境下大概率超时。如果你用 vscode 做开发装完依赖后在 vscode 里按 CtrlShiftP输入「Python: Select Interpreter」选中刚才创建的 plate_env 环境这样终端和调试器都会用同一个解释器不会出现「终端能跑、调试报错」的玄学问题。2.3 验证环境是否就绪装完之后跑一段最小验证代码确认 torch 和 opencv 都能正常导入并且 torch 能识别到 GPU如果有的话。import torch import cv2 import numpy as np # 检查 torch 版本和 CUDA 可用性 print(fPyTorch version: {torch.__version__}) print(fCUDA available: {torch.cuda.is_available()}) if torch.cuda.is_available(): print(fGPU device: {torch.cuda.get_device_name(0)}) # 检查 opencv print(fOpenCV version: {cv2.__version__}) # 简单测试张量运算 x torch.randn(1, 3, 640, 640) print(fTensor shape: {x.shape}, device: {x.device})这段代码的逻辑很直接先确认 torch 版本号避免装成了旧版再检查 CUDA 是否可用如果返回 False 但你有独显大概率是 CUDA 版本和 torch 版本不匹配最后用 opencv 版本号确认图像处理库没问题。如果这三项都正常输出环境就算搭好了。3. YOLOv11 车牌检测训练自己的模型与推理结果保存3.1 数据集结构与标注格式这份资源里的数据集一般按 YOLO 标准格式组织images 目录放原图labels 目录放同名 txt 标注文件每行格式是class_id x_center y_center width height坐标都归一化到 0 到 1 之间。车牌检测通常只有一个类别所以 class_id 全是 0。目录结构大概长这样dataset/ ├── images/ │ ├── train/ │ │ ├── 001.jpg │ │ └── ... │ └── val/ │ └── ... ├── labels/ │ ├── train/ │ │ ├── 001.txt │ │ └── ... │ └── val/ │ └── ... └── data.yamldata.yaml 是训练配置的入口文件内容需要包含训练集和验证集路径、类别数和类别名。# data.yaml path: ./dataset train: images/train val: images/val nc: 1 names: [plate]这里nc是类别数车牌检测只有一类所以写 1。names列表里的名字要和标注时的类别对应写错了训练不会报错但推理时类别名会显示异常。3.2 启动训练与关键参数YOLOv11 的训练入口在 ultralytics 框架下命令行和 Python 脚本两种方式都行。我一般用 Python 脚本方便记录参数。from ultralytics import YOLO # 加载预训练权重没有预训练权重就从 yaml 构建 model YOLO(yolo11n.pt) # 用 nano 版本速度快适合毕业设计演示 # 开始训练 results model.train( datadata.yaml, # 数据集配置文件路径 epochs100, # 训练轮数车牌检测一般 80-120 轮收敛 imgsz640, # 输入图像尺寸640 是精度和速度的平衡点 batch16, # 批大小显存不够就降到 8 或 4 device0, # GPU 编号没有 GPU 写 cpu workers4, # 数据加载线程数 patience20, # 早停耐心值20 轮没提升就停 saveTrue, # 保存训练权重 projectruns/train, # 输出目录 nameplate_det # 实验名称 )参数里最需要关注的是imgsz和batch。车牌属于小目标imgsz设太小比如 320会导致车牌在特征图上只剩几个像素召回率断崖式下降。640 是经过验证的合理值。batch受显存限制8G 显存跑 640 尺寸大概能开到 16再大就 OOM 了。patience设 20 的意思是如果连续 20 轮验证集 mAP 没有提升就自动停止避免过拟合。训练完成后权重文件保存在runs/train/plate_det/weights/best.pt这个文件后面推理和部署都要用。3.3 推理与保存检测结果训练完之后最直接的需求就是拿一张图跑推理把检测到的车牌框出来并保存。YOLOv11 的推理接口很简洁但保存结果有几个细节容易翻车。from ultralytics import YOLO import cv2 # 加载训练好的权重 model YOLO(runs/train/plate_det/weights/best.pt) # 对单张图片推理 results model.predict( sourcetest_image.jpg, # 输入图片路径 conf0.5, # 置信度阈值低于这个值的框会被过滤 iou0.45, # NMS 的 IoU 阈值 saveTrue, # 保存带标注的结果图 save_txtTrue, # 保存检测框坐标到 txt projectruns/detect, # 输出目录 nameresult # 结果子目录名 ) # 手动提取检测框并裁剪车牌区域 for r in results: boxes r.boxes for i, box in enumerate(boxes): # 获取坐标xyxy 格式 x1, y1, x2, y2 box.xyxy[0].cpu().numpy().astype(int) conf box.conf[0].cpu().numpy() # 读取原图并裁剪 img cv2.imread(test_image.jpg) plate_img img[y1:y2, x1:x2] # 保存裁剪出的车牌 cv2.imwrite(fplate_crop_{i}.jpg, plate_img) print(fPlate {i}: bbox({x1},{y1},{x2},{y2}), conf{conf:.3f})这段代码做了两件事先用model.predict跑推理并保存可视化结果再手动遍历检测框把车牌区域裁出来。裁剪这一步是连接 YOLOv11 和 LPRNet 的关键——LPRNet 的输入是单张车牌图片不是整张场景图。conf参数设 0.5 是保守值实际调的时候如果发现漏检多就降到 0.3误检多就升到 0.6。iou控制 NMS 的合并阈值车牌之间一般不会重叠0.45 够用。save_txtTrue会把每个框的坐标存成 txt格式和训练标注一致方便后续做误差分析。注意裁剪时 x1、y1、x2、y2 要转成 int 类型numpy 的 float 直接做切片索引会报 TypeError。另外要检查坐标是否越界x2 超过图像宽度时要截断到 img.shape[1]。4. LPRNet 字符识别从车牌裁剪图到字符序列4.1 LPRNet 的 CTC 解码原理LPRNet 的核心是 CTCConnectionist Temporal Classification损失函数。传统做法是先分割字符再逐个识别但车牌图像里字符间距不均匀、有铆钉干扰、光照变化大分割本身就是一个容易出错的环节。CTC 的思路是我不需要知道每个字符具体在哪个位置只需要知道整张图对应的字符序列是什么。网络输出的是一个 T×C 的矩阵T 是时间步数可以理解为把特征图按宽度展开后的列数C 是字符集大小包含空白符。解码时用贪心策略每个时间步取概率最大的字符然后合并连续重复字符并去掉空白符。比如输出序列是-京-A-京-京-1-2-3-去掉空白和重复后就得到京A京123。LPRNet 的字符集通常包含省份简称京、沪、粤等、字母 A-Z去掉 I 和 O 避免和 1、0 混淆、数字 0-9加上 CTC 的空白符总共 68 个左右。具体字符集定义在源码的lprnet.py或chars.py里训练和推理必须用同一套字符集否则解码结果全是乱码。4.2 加载模型并推理LPRNet 的推理代码需要先做图像预处理把裁剪出的车牌缩放到固定尺寸通常是 94×24归一化后转成张量。import torch import cv2 import numpy as np from model.lprnet import LPRNet, CHARS # 根据实际源码路径调整 # 字符集必须和训练时一致 chars CHARS # 加载模型 lprnet LPRNet(class_numlen(chars), dropout_rate0) lprnet.load_state_dict(torch.load(weights/lprnet_best.pth, map_locationcpu)) lprnet.eval() def preprocess_plate(plate_img): 将裁剪的车牌图预处理为 LPRNet 输入格式 # 缩放到 94x24 img cv2.resize(plate_img, (94, 24)) # BGR 转 RGB img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 归一化到 [-1, 1] img (img.astype(np.float32) / 255.0 - 0.5) / 0.5 # HWC 转 CHW增加 batch 维度 img np.transpose(img, (2, 0, 1)) img np.expand_dims(img, axis0) return torch.from_numpy(img) def decode_ctc(output, chars): CTC 贪心解码 # output shape: (1, T, C) output output.argmax(dim2).squeeze().cpu().numpy() result [] prev -1 for idx in output: if idx ! prev and idx len(chars): result.append(chars[idx]) prev idx return .join(result) # 推理 plate_img cv2.imread(plate_crop_0.jpg) input_tensor preprocess_plate(plate_img) with torch.no_grad(): output lprnet(input_tensor) plate_text decode_ctc(output, chars) print(f识别结果: {plate_text})预处理里最关键的是归一化方式。LPRNet 训练时用的是(pixel/255 - 0.5) / 0.5把像素值映射到 -1 到 1。推理时必须用同样的公式用 ImageNet 的均值和方差反而会掉点。缩放尺寸 94×24 也是固定的因为网络最后几层的全连接维度是按这个尺寸设计的改了尺寸会报维度不匹配。解码函数里prev变量用来跳过连续重复字符这是 CTC 解码的标准操作。注意idx len(chars)这个判断防止索引越界——虽然正常情况下不会出现但模型输出异常时能兜住。4.3 两阶段串联的完整流程把 YOLOv11 和 LPRNet 串起来完整流程是读入场景图 → YOLOv11 检测车牌位置 → 裁剪车牌区域 → 预处理 → LPRNet 推理 → CTC 解码 → 输出车牌号。def recognize_plate(image_path, det_model, rec_model, chars): 端到端车牌识别 img cv2.imread(image_path) # 第一阶段检测 results det_model.predict(sourceimg, conf0.5, verboseFalse) plates [] for r in results: for box in r.boxes: x1, y1, x2, y2 box.xyxy[0].cpu().numpy().astype(int) # 边界保护 x1, y1 max(0, x1), max(0, y1) x2, y2 min(img.shape[1], x2), min(img.shape[0], y2) plate_img img[y1:y2, x1:x2] if plate_img.size 0: continue # 第二阶段识别 input_tensor preprocess_plate(plate_img) with torch.no_grad(): output rec_model(input_tensor) text decode_ctc(output, chars) plates.append({bbox: (x1, y1, x2, y2), text: text}) return plates # 使用 det_model YOLO(runs/train/plate_det/weights/best.pt) rec_model LPRNet(class_numlen(chars), dropout_rate0) rec_model.load_state_dict(torch.load(weights/lprnet_best.pth, map_locationcpu)) rec_model.eval() results recognize_plate(test_image.jpg, det_model, rec_model, chars) for p in results: print(f车牌位置: {p[bbox]}, 车牌号: {p[text]})这段串联代码里加了边界保护因为 YOLOv11 输出的框有可能超出图像范围尤其是边缘处的车牌不截断的话切片会得到空数组。verboseFalse是关掉 ultralytics 的详细日志输出批量处理时能清爽不少。5. 避坑与排查训练不收敛、识别乱码、部署报错5.1 训练 loss 不下降或震荡现象YOLOv11 训练前几轮 loss 正常下降到第 10 轮左右开始震荡mAP 卡在 0.3 上不去。原因最常见的是学习率设太大。YOLOv11 默认用 SGD 优化器初始学习率 0.01如果数据集规模小比如只有几百张图这个学习率会导致梯度更新过猛loss 在最优值附近来回跳。解决把学习率降到 0.001同时加 warmup。在 train 参数里加lr00.001和warmup_epochs3。另外检查一下标注文件里有没有空 txt 或者坐标超出 0 到 1 范围的异常值这种脏数据也会导致 loss 异常。5.2 LPRNet 识别结果全是重复字符现象推理出来是「京京京京京」或者「AAAAAAA」这种。原因CTC 解码时没有正确去掉空白符。LPRNet 输出的字符集里索引 0 通常是空白符blank如果解码时把空白符也当成有效字符输出就会出现连续重复。解决检查decode_ctc函数里字符集的顺序。空白符必须在索引 0 的位置解码时遇到索引 0 要跳过。另外确认训练时的字符集和推理时的字符集完全一致顺序都不能差。5.3 裁剪出的车牌图是空的或全黑现象YOLOv11 检测到了框但裁剪保存的图片打开是全黑或者尺寸为 0。原因坐标越界。YOLOv11 输出的 xyxy 坐标是浮点数直接转 int 时如果 x2 等于图像宽度切片img[y1:y2, x1:x2]会得到空数组。另外如果 x1 大于 x2极少数情况下的异常框也会出问题。解决裁剪前做边界钳制x1 max(0, min(x1, w-1))x2 max(x11, min(x2, w))y 方向同理。裁剪后加一个if plate_img.size 0: continue的判断。5.4 模型加载报 KeyError 或 size mismatch现象load_state_dict时报 key 不匹配或者某个层的权重尺寸对不上。原因训练时用的网络结构和推理时构建的网络结构不一致。常见于改了dropout_rate或者class_num但没重新训练。解决确认推理时LPRNet(class_numlen(chars), dropout_rate0)的参数和训练时完全一致。dropout_rate在推理时必须设为 0否则会随机丢弃神经元导致结果不稳定。如果字符集改过必须重新训练模型不能直接用旧权重。5.5 GPU 显存不足导致训练中断现象训练到一半报CUDA out of memory。原因batch size 太大或者imgsz设太高或者 dataloader 的 workers 太多导致内存泄漏。解决先把 batch 减半如果还不行就把imgsz从 640 降到 512。另外在 train 参数里加ampTrue开启混合精度训练能省大约 30% 显存。workers设成 2 或 4 就行设太大反而容易出问题。6. 进阶技巧用 ONNX 导出把推理速度再压一截训练和验证都跑通之后如果要做演示或者部署到边缘设备PyTorch 原生推理的速度往往不够看。我一般会把两个模型都导出成 ONNX 格式用 onnxruntime 做推理CPU 上大概能快 1.5 到 2 倍GPU 上差距小一些但显存占用更低。YOLOv11 导出 ONNX 很简单ultralytics 自带接口from ultralytics import YOLO model YOLO(runs/train/plate_det/weights/best.pt) model.export(formatonnx, imgsz640, opset12, simplifyTrue)opset12是 ONNX 算子集版本12 兼容性最好。simplifyTrue会调用 onnx-simplifier 做图优化去掉冗余节点。导出后的 onnx 文件在权重同目录下。LPRNet 导出 ONNX 需要手动指定输入输出名import torch from model.lprnet import LPRNet, CHARS model LPRNet(class_numlen(CHARS), dropout_rate0) model.load_state_dict(torch.load(weights/lprnet_best.pth, map_locationcpu)) model.eval() # 构造示例输入 dummy_input torch.randn(1, 3, 24, 94) torch.onnx.export( model, dummy_input, lprnet.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}}, opset_version12 )dynamic_axes把 batch 维度设为动态这样同一个 onnx 文件可以处理单张也可以处理批量。导出后用 onnxruntime 加载验证一下import onnxruntime as ort import numpy as np sess ort.InferenceSession(lprnet.onnx) input_name sess.get_inputs()[0].name # 用同样的预处理数据测试 test_input np.random.randn(1, 3, 24, 94).astype(np.float32) output sess.run(None, {input_name: test_input}) print(fONNX output shape: {output[0].shape})输出 shape 应该是(1, T, 68)左右T 是时间步数。如果 shape 不对检查导出时的 opset 版本和 onnxruntime 版本是否匹配。有个细节值得单独说ONNX 推理时预处理必须和 PyTorch 完全一致包括归一化参数、通道顺序、尺寸。我见过有人导出后识别率暴跌排查半天发现是 onnx 推理时忘了做 BGR 到 RGB 的转换。这种问题没有报错就是结果不对属于典型的黑匣子问题。从那以后我每次导出 ONNX 都会用同一张测试图分别跑 PyTorch 和 ONNX 推理对比输出文本是否一致确认无误再往下走。这个习惯帮我省了不少返工时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站