YOLOv8+LPRNet:从零搭建中文车牌识别系统实战
简介这份资源是面向计算机、数学、电子信息等专业学生的毕业设计与课程设计参考资料核心为基于 YOLOv8 与 LPRNet 的车牌识别系统提供完整 Python 源码与训练好的模型权重下载后可直接运行调试。项目采用 Flask 搭建后端服务前端使用 Vue 与 Vite 构建可视化界面并配套 Dockerfile 与依赖清单便于环境部署与二次开发。压缩包共 60 个文件约 31.35MB涵盖 13 个 py 脚本、3 个 pt 模型文件、1 个 pth 权重、18 张 jpg 示例图以及 json、vue、js、md、yml 等配置与说明文档覆盖数据处理、模型训练、推理部署全流程。目前已有 606 人学习下载。读者可借此掌握车牌检测与字符识别的联合方案理解 YOLOv8 目标检测与 LPRNet 序列识别的衔接逻辑并参考目录结构快速定位训练脚本、推理入口与前端页面适合作为毕设模板或功能扩展的起点。1. 从一张歪着拍的车牌说起YOLOv8 LPRNet 到底解决了什么地下车库出口车斜着停车牌被泥点糊了一半顶灯还在玻璃上反光。这种图丢给传统 OpenCV 轮廓法基本当场翻车。而 YOLOv8 加 LPRNet 这套组合恰好是冲着这种真实场景来的YOLOv8 负责在整张图里把车牌框出来LPRNet 负责把框出来的小图逐字符读成文本。一个管在哪一个管是什么职责切得很干净。这套方案适合谁做毕业设计的学生、想入门目标检测加 OCR 联调的工程师、需要给停车场或门禁做原型验证的开发者。它不需要你先精通 Transformer也不需要标注工具用到飞起只要会配 Python 环境、能跑通训练脚本就能从零搭出一套能识别中文车牌的完整链路。下面我按环境怎么搭、数据怎么处理、两个模型怎么串、坑在哪的顺序把这条链路讲透。2. 环境搭建与依赖选型CPU 也能跑通的最小闭环2.1 为什么先锁 Python 版本和 PyTorch 组合这套方案最容易翻车的地方不是模型是环境。YOLOv8 依赖 ultralytics 包LPRNet 是纯 PyTorch 手写网络两者对 PyTorch 版本的要求必须统一。我一般锁 Python 3.9 或 3.10PyTorch 选 2.0 以上的稳定版。Python 3.11 早期版本和某些 torch 轮子有兼容问题新手别去踩。CPU 版本完全能跑通推理和少量训练只是慢。如果你手上有 GTX1660Ti 这类 6G 显存的卡训练 YOLOv8n 绰绰有余没有也能用 CPU 先验证整条链路再决定要不要上显卡。RK3588、Orin 这类板端部署是后话先把 PC 端跑通再说。# 创建独立环境避免污染系统 Python conda create -n lpr python3.9 -y conda activate lpr # 安装 PyTorchCPU 版本用这个源有 CUDA 的按官网命令换 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 安装 YOLOv8 官方包和常用图像库 pip install ultralytics opencv-python numpy pillow pyyaml tqdm这段命令的逻辑是先隔离环境再装框架最后装业务库。ultralytics一个包就把 YOLOv8 的训练、验证、导出全带上了不用单独 clone 仓库。opencv-python负责图像读写和预处理pyyaml用来读数据配置文件。参数上唯一要注意的是 PyTorch 源地址CPU 版和 CUDA 版命令不同装错了会在torch.cuda.is_available()上返回 False训练时白白等半天。2.2 验证环境是否真的可用装完别急着跑训练先做三步验证能省掉后面大量排查时间。import torch import ultralytics import cv2 print(torch:, torch.__version__) print(cuda available:, torch.cuda.is_available()) print(ultralytics:, ultralytics.__version__) print(opencv:, cv2.__version__) # 快速验证 YOLOv8 能否加载预训练权重 from ultralytics import YOLO model YOLO(yolov8n.pt) print(model loaded, names:, len(model.names))逻辑说明前三行打印版本确认没有装串。YOLO(yolov8n.pt)会自动下载官方预训练权重能加载成功说明网络和包都没问题。如果卡在下载多半是网络问题可以手动下载权重放到当前目录再加载。参数上yolov8n是最小的 nano 版本适合先跑通流程正式训练车牌检测时数据量够的话可以换yolov8s或yolov8m精度会高一些但显存和耗时也上去了。提示CPU 环境下训练 YOLOv8n 一个 epoch 可能要几分钟先用 5 到 10 张图跑 2 个 epoch 验证流程别一上来就全量训练。3. 车牌数据集处理从标注到 YOLOv8 能吃的格式3.1 标注工具选型与标注规范车牌检测的标注和普通目标检测没区别用 labelme 或 labelImg 都行画框、选类别、导出。但车牌有个特殊点框要贴紧车牌边缘不要留太多背景否则 LPRNet 拿到的图里混入车身文字识别会乱。我一般要求标注框比车牌实际边缘外扩 2 到 3 个像素既包住完整字符又不引入多余区域。类别名统一用plate就行别搞blue_plate、green_plate这种细分颜色分类交给后处理逻辑判断检测模型只负责这是不是车牌。标注完导出成 YOLO 格式每张图对应一个 txt每行是类别id 中心x 中心y 宽 高全部归一化到 0 到 1。3.2 数据集目录结构与 YAML 配置YOLOv8 对目录结构有固定要求常见做法是这样组织datasets/ plate/ images/ train/ # 训练图片 val/ # 验证图片 labels/ train/ # 对应 txt val/ data.yamldata.yaml内容如下path: ./datasets/plate train: images/train val: images/val nc: 1 names: [plate]逻辑说明path是数据集根目录train和val是相对路径。nc是类别数车牌检测只有一类所以是 1。names必须和标注时的类别顺序一致写错了训练不报错但结果全乱。参数上唯一容易错的是路径分隔符Windows 下用反斜杠有时会出问题统一用正斜杠最稳。3.3 用脚本做数据清洗和格式校验标注完的数据经常有脏样本空 txt、坐标越界、图片和标签对不上。写个脚本过一遍比训练时报错再回头查省事得多。import os from pathlib import Path img_dir Path(datasets/plate/images/train) lbl_dir Path(datasets/plate/labels/train) bad [] for img in img_dir.glob(*.jpg): lbl lbl_dir / (img.stem .txt) if not lbl.exists(): bad.append((missing_label, img.name)) continue lines lbl.read_text().strip().splitlines() if not lines: bad.append((empty_label, img.name)) continue for line in lines: parts line.split() if len(parts) ! 5: bad.append((bad_format, img.name)) break coords list(map(float, parts[1:])) if any(c 0 or c 1 for c in coords): bad.append((out_of_range, img.name)) break print(问题样本数:, len(bad)) for item in bad[:20]: print(item)逻辑说明遍历所有训练图检查标签文件是否存在、是否为空、每行是否 5 个字段、坐标是否在 0 到 1 之间。bad列表收集所有异常打印前 20 条方便定位。参数上glob(*.jpg)只匹配 jpg如果你的图是 png 要改。这个脚本不自动修复只做体检修复逻辑根据具体问题单独写避免误删有效数据。注意验证集和训练集要来自不同场景比如训练集用车库图验证集用路边图否则验证精度虚高实际部署就露馅。4. YOLOv8 训练车牌检测参数怎么设、曲线怎么看4.1 训练命令与关键参数含义数据准备好后训练就一行命令的事但参数设不对结果差很多。yolo detect train \ datadatasets/plate/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectruns/plate \ nameexp1逻辑说明data指向配置文件model用预训练权重做迁移学习比从零训练收敛快得多。epochs是总轮数车牌这种单类任务 100 轮通常够。imgsz640是输入尺寸车牌在图中占比小的话可以提到 960但显存和耗时增加。batch16在 6G 显存上跑 640 尺寸差不多是上限显存不够就降到 8。lr0是初始学习率0.01 是官方默认数据量小可以降到 0.001 防止震荡。patience20表示 20 轮没提升就早停省时间。4.2 损失曲线和指标怎么看训练完在runs/plate/exp1下有results.csv和一堆曲线图。重点看三个train/box_loss是否稳定下降、metrics/mAP50是否收敛、val/box_loss有没有反弹。如果训练损失降但验证损失升就是过拟合加数据或加增强。mAP50 到 0.95 以上车牌检测基本够用。import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/plate/exp1/results.csv) df.columns df.columns.str.strip() plt.plot(df[epoch], df[train/box_loss], labeltrain_box) plt.plot(df[epoch], df[val/box_loss], labelval_box) plt.plot(df[epoch], df[metrics/mAP50], labelmAP50) plt.legend() plt.xlabel(epoch) plt.savefig(curve.png)逻辑说明读训练日志把三条关键曲线画在一张图上。columns.str.strip()是因为 csv 列名有时带空格不去掉会 KeyError。参数上metrics/mAP50是 IoU 0.5 时的平均精度车牌检测看这个就够不用太纠结 mAP50-95。4.3 推理验证把检测框画出来训练完先别急着接 LPRNet单独验证 YOLOv8 的检测效果。from ultralytics import YOLO import cv2 model YOLO(runs/plate/exp1/weights/best.pt) results model(test.jpg, conf0.4, iou0.5) for r in results: for box in r.boxes: x1, y1, x2, y2 map(int, box.xyxy[0]) cv2.rectangle(r.orig_img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(detected.jpg, r.orig_img)逻辑说明conf0.4是置信度阈值低于这个的框丢掉iou0.5是 NMS 的 IoU 阈值控制重叠框合并。车牌场景下conf可以设到 0.5 减少误检iou保持 0.5 左右。box.xyxy是左上右下坐标直接拿来裁剪就是 LPRNet 的输入。5. LPRNet 识别车牌字符从裁剪图到文本输出5.1 LPRNet 的网络结构和输入要求LPRNet 是个轻量级端到端网络不用先做字符分割直接输出字符序列。它的核心是 CNN 提特征加 CTC 解码输入尺寸固定为 94x24灰度或彩色都行但训练和推理必须一致。中文车牌 7 个字符加上 CTC 的 blank输出序列长度一般设 18 到 24 之间。网络结构上前面是几个卷积和池化层降维中间接一个全连接最后按时间步输出每个位置的字符概率。CTC 负责把重复字符和 blank 去掉得到最终文本。这套结构的好处是对字符粘连、轻微倾斜有容忍度比传统分割加单字识别稳。5.2 字符集定义和标签编码中文车牌字符集包括省份简称、字母、数字一共 60 多个。定义好字符集是训练的前提。CHARS [ 京, 津, 冀, 晋, 蒙, 辽, 吉, 黑, 沪, 苏, 浙, 皖, 闽, 赣, 鲁, 豫, 鄂, 湘, 粤, 桂, 琼, 渝, 川, 贵, 云, 藏, 陕, 甘, 青, 宁, 新, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, G, H, J, K, L, M, N, P, Q, R, S, T, U, V, W, X, Y, Z ] char2idx {c: i for i, c in enumerate(CHARS)} idx2char {i: c for c, i in char2idx.items()}逻辑说明CHARS顺序一旦定下就不能改训练和推理必须用同一份。char2idx用于把标签文本转成索引序列idx2char用于推理时把索引还原成文本。参数上注意车牌里没有字母 I 和 O避免和数字 1、0 混淆字符集里要去掉。5.3 训练 LPRNet 的最小脚本LPRNet 训练数据是裁剪好的车牌小图加对应文本标签。写个 Dataset 类读数据再套标准训练循环。import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader from PIL import Image import numpy as np class PlateDataset(Dataset): def __init__(self, img_list, char2idx): self.items img_list # [(img_path, label_text), ...] self.char2idx char2idx def __len__(self): return len(self.items) def __getitem__(self, idx): path, text self.items[idx] img Image.open(path).convert(RGB).resize((94, 24)) img np.array(img, dtypenp.float32) / 255.0 img img.transpose(2, 0, 1) # HWC - CHW label [self.char2idx[c] for c in text if c in self.char2idx] return torch.tensor(img), torch.tensor(label) def collate_fn(batch): imgs, labels zip(*batch) imgs torch.stack(imgs) target_lengths torch.tensor([len(l) for l in labels]) targets torch.cat(labels) return imgs, targets, target_lengths逻辑说明resize((94, 24))是 LPRNet 固定输入不能改。transpose(2, 0, 1)把通道维换到前面符合 PyTorch 要求。collate_fn处理变长标签CTC 损失需要把标签拼成一维加长度信息。参数上target_lengths记录每条标签的真实长度CTC 靠它区分不同样本。训练循环里用nn.CTCLoss()优化器选 Adam学习率 0.001跑 50 到 100 轮。损失降到 0.1 以下字符准确率通常能到 95% 以上。5.4 把 YOLOv8 和 LPRNet 串起来两个模型各自跑通后串联逻辑很简单YOLOv8 出框按框裁剪裁剪图送 LPRNet输出文本。def recognize(img_path, det_model, rec_model, idx2char): results det_model(img_path, conf0.5, iou0.5) plates [] for r in results: for box in r.boxes: x1, y1, x2, y2 map(int, box.xyxy[0]) crop r.orig_img[y1:y2, x1:x2] crop cv2.resize(crop, (94, 24)) crop crop[:, :, ::-1].transpose(2, 0, 1) # BGR-RGB, HWC-CHW crop torch.tensor(crop[None], dtypetorch.float32) / 255.0 with torch.no_grad(): pred rec_model(crop) text ctc_decode(pred, idx2char) plates.append((text, (x1, y1, x2, y2))) return plates逻辑说明crop是检测框内的车牌图resize 到 LPRNet 输入尺寸。[:, :, ::-1]把 OpenCV 的 BGR 转成 RGB顺序错了识别结果会乱。ctc_decode是贪心解码取每个时间步最大概率的字符去重去 blank。参数上conf0.5比单模型验证时略高因为串联后误检代价更大。6. 避坑与排查这套组合最容易翻车的五个地方6.1 检测框偏一点识别全乱现象YOLOv8 框出的车牌边缘切掉了第一个或最后一个字符LPRNet 输出少一位。原因标注时框太紧或检测模型对边缘回归不准。解决标注时外扩 2 到 3 像素推理裁剪时再按框宽高各扩 5%给识别留余量。6.2 中文省份简称识别率明显低于字母数字现象字母数字基本对省份简称经常错。原因训练数据里省份分布不均某些省份样本太少。解决统计各字符出现频次对低频字符做过采样或在损失里给低频字符加权。数据量够的话每个省份至少 200 张。6.3 训练损失不降一直卡在高位现象LPRNet 训练几十轮loss 在 5 以上不动。原因字符集和标签对不上或 CTC 的 blank 索引设错。解决检查char2idx是否覆盖所有标签字符CTC 的 blank 一般设成len(CHARS)别和已有字符索引冲突。打印几条标签的编码结果肉眼确认。6.4 推理速度慢CPU 上单张要好几秒现象CPU 推理一张图超过 2 秒。原因YOLOv8 用了大模型或 LPRNet 没做 batch 推理。解决检测换yolov8n识别把多张裁剪图拼成一个 batch 一次前向。CPU 上还可以用 ONNX Runtime 加速导出 ONNX 后推理能快 2 到 3 倍。6.5 验证集精度高实际场景一塌糊涂现象验证集 mAP 0.98实际拍图识别率不到一半。原因验证集和训练集同分布没覆盖真实场景的光照、角度、模糊。解决验证集必须来自独立场景训练时加随机亮度、对比度、轻微旋转增强。真实场景测试至少准备 50 张不同条件的图别只看验证集数字。7. 进阶技巧用 ONNX 导出把推理速度压下来两个模型都跑通后性能优化是下一步。最直接的办法是把 YOLOv8 和 LPRNet 都导出成 ONNX用 ONNX Runtime 推理CPU 上通常能快 2 到 3 倍板端部署也走这条路。from ultralytics import YOLO import torch # 导出 YOLOv8 为 ONNX det YOLO(runs/plate/exp1/weights/best.pt) det.export(formatonnx, imgsz640, simplifyTrue) # 导出 LPRNet 为 ONNX rec LPRNet(len(CHARS) 1) rec.load_state_dict(torch.load(lprnet_best.pth, map_locationcpu)) rec.eval() dummy torch.randn(1, 3, 24, 94) torch.onnx.export( rec, dummy, lprnet.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}}, opset_version11 )逻辑说明YOLOv8 的export方法一行搞定simplifyTrue会做图优化。LPRNet 用torch.onnx.exportdynamic_axes让 batch 维可变方便批量推理。opset_version11兼容性最好板端 NPU 一般支持到 11 或 12。参数上dummy的 shape 必须和实际输入一致LPRNet 是(1, 3, 24, 94)注意高在前宽在后。导出后用 ONNX Runtime 加载验证import onnxruntime as ort import numpy as np sess ort.InferenceSession(lprnet.onnx, providers[CPUExecutionProvider]) inp np.random.randn(1, 3, 24, 94).astype(np.float32) out sess.run(None, {input: inp})[0] print(output shape:, out.shape)逻辑说明providers指定 CPU 执行有 GPU 可以换CUDAExecutionProvider。out的 shape 是(batch, time_steps, num_classes)后面接 CTC 解码。参数上注意 ONNX 推理的输入 dtype 必须是 float32uint8 会报错。我自己的习惯是PC 端先用 PyTorch 调通精度再导 ONNX 压速度最后才考虑板端。跳过 ONNX 直接上板端出问题很难定位是模型问题还是部署问题。这套 YOLOv8 加 LPRNet 的组合链路清晰、依赖少、可复现性强作为毕业设计或原型验证都够扎实。希望帮到你。本文还有配套的精品资源点击获取