基于YOLOv8和LPRNet的车牌识别系统:从训练到部署的完整指南
发布时间:2026/9/27 1:08:18来源:尧图网络
简介本资源是一套面向计算机、电子信息等专业学生的车牌识别系统毕业设计完整源码包基于 YOLOv8 完成车牌检测、LPRNet 实现字符识别适合作为课程设计、期末大作业或毕设参考项目也适合想入门深度学习目标检测与OCR的开发者学习借鉴。压缩包共60个文件约31.35MB包含13个Python源码文件、3个pt模型权重、1个pth权重、3个Vue前端文件以及json配置、Dockerfile、markdown说明等覆盖Flask后端服务、YOLOv8训练脚本、LPRNet数据生成与标注处理、前端可视化界面及Docker部署配置目录结构清晰便于按模块阅读与二次开发。目前已有606人学习下载。读者可从中获得一套可直接运行的完整车牌识别方案理解检测与识别两阶段的模型训练、数据处理与前后端联调思路并借助现成权重快速验证效果为后续功能扩展与调试提供扎实的代码基础。1. 车牌识别系统为什么总在“最后一公里”翻车YOLOv8 加 LPRNet 的真实分工做车牌识别很多人第一反应是“检测加识别两个模型串起来就完事”。真跑起来才发现检测框稍微歪一点、光线暗一点、车牌脏一点识别结果就变成一串乱码。这套基于 YOLOv8 和 LPRNet 的车牌识别系统核心思路是把“找车牌”和“读车牌”拆成两个独立阶段YOLOv8 负责在整张图里定位车牌区域LPRNet 负责把裁出来的车牌小图转成字符序列。这种两阶段方案在毕业设计里非常常见因为它既能体现目标检测能力又能展示序列识别模型的设计工作量饱满且可解释性强。适合谁看如果你正在做基于 YOLOv8 的毕业设计或者想用 Python 搭一套能跑通训练、推理、评估全流程的车牌识别系统这篇笔记会按“环境怎么配、数据怎么整、模型怎么训、坑怎么避”的顺序讲清楚。我不会假装手里有某个现成压缩包而是把这类项目最常见的落地路径拆开让你能照着复现也能判断哪些环节值得投入时间。2. 把 YOLOv8 和 LPRNet 串成流水线先搞清楚数据怎么流2.1 两阶段分工检测框和字符序列之间差了什么YOLOv8 输出的是车牌的四点坐标或矩形框LPRNet 需要的是固定高度、宽度可变的车牌裁剪图。中间这一步裁剪和预处理往往是新手最容易忽略的地方。常见做法是YOLOv8 检测到车牌后按框裁剪再把裁剪图缩放到高度 48 像素、宽度按比例缩放最后归一化送进 LPRNet。如果裁剪时框偏了LPRNet 看到的就不是完整车牌识别率会断崖式下跌。LPRNet 本身是一个轻量级卷积网络它不依赖循环神经网络而是用 CTC 损失直接输出字符序列。这意味着你不需要对车牌字符做精确分割只要给出整张车牌图和对应的字符标签即可。对于中国车牌字符集通常包括省份简称、字母、数字和特殊字符总共约 68 类。训练时标签写成字符串比如“京A12345”LPRNet 会自己学习对齐。提示YOLOv8 和 LPRNet 的输入尺寸要匹配好。YOLOv8 常用 640×640 训练LPRNet 常用 94×24 或 48×168 之类的尺寸裁剪后一定要按 LPRNet 的要求缩放不要直接把 YOLOv8 的输入图丢进去。2.2 数据准备从标注到 YOLO 格式和 LPRNet 标签车牌数据集一般有两种标注一种是检测框一种是字符内容。如果你手头只有整图需要先用 labelme 或类似工具标出车牌框再人工录入车牌号码。检测标注转成 YOLO 格式时每张图对应一个 txt 文件每行是class_id x_center y_center width height坐标都要归一化到 0 到 1 之间。# 将 labelme 格式的 JSON 转成 YOLO 检测标签 import json import os def labelme_to_yolo(json_path, output_dir, class_map): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w data[imageWidth] img_h data[imageHeight] lines [] for shape in data[shapes]: label shape[label] if label not in class_map: continue points shape[points] xs [p[0] for p in points] ys [p[1] for p in points] x_center (min(xs) max(xs)) / 2.0 / img_w y_center (min(ys) max(ys)) / 2.0 / img_h width (max(xs) - min(xs)) / img_w height (max(ys) - min(ys)) / img_h lines.append(f{class_map[label]} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) base os.path.splitext(os.path.basename(json_path))[0] with open(os.path.join(output_dir, base .txt), w) as f: f.write(\n.join(lines))这段代码把 labelme 的矩形或多边形标注转成 YOLO 需要的归一化中心点和宽高。class_map里只需要一个类别比如{plate: 0}。转换后检查一下有没有宽高为 0 的异常框那种通常是标注时点重合了。LPRNet 的标签更简单一个图片路径对应一个字符串。你可以建一个train.txt每行写成images/xxx.jpg 京A12345。注意字符集要固定训练和推理必须一致。常见做法是维护一个chars.txt里面按顺序放所有可能出现的字符LPRNet 输出索引后查表还原。2.3 环境搭建Ubuntu 20.04 下 CPU 版也能先跑通热词里很多人搜“ubuntu20.04搭建yolov8环境cpu版本”说明不是所有人都有显卡。CPU 版跑训练会非常慢但跑通推理和验证流程完全够用。我一般会先建一个干净的 conda 环境Python 选 3.8 或 3.9然后装 ultralytics 和 torch 的 CPU 版本。conda create -n plate python3.9 -y conda activate plate pip install ultralytics torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install opencv-python numpy pillow装完后用yolo checks看一下环境是否正常。如果后面要训练 LPRNet还需要装 pytorch 的 CTC 相关依赖不过 LPRNet 本身不复杂用标准 torch 就能实现。CPU 训练 YOLOv8 时把workers调小batch调到 4 或 8否则内存容易爆。注意不要混用 conda 和 pip 装 torch很容易出现版本冲突。如果已经装乱了删掉环境重来比修依赖快。3. 训练 YOLOv8 车牌检测器参数怎么设、曲线怎么看3.1 用命令行还是 Python 脚本两种训练入口的取舍YOLOv8 提供了命令行和 Python API 两种训练方式。命令行适合快速试参Python 脚本适合把训练逻辑嵌进项目里。毕业设计里我建议用 Python 脚本因为后面要接 LPRNet统一在代码里管理路径和参数更方便。from ultralytics import YOLO model YOLO(yolov8n.pt) # 用预训练权重起步 results model.train( dataplate.yaml, epochs100, imgsz640, batch8, workers2, devicecpu, lr00.01, patience20, projectruns/plate, nameyolov8n_plate )data指向数据集配置文件里面写清楚训练、验证、测试集的路径和类别数。epochs先设 100 看收敛情况patience设 20 表示 20 轮没提升就早停。lr0是初始学习率CPU 训练时可以稍微调小到 0.005避免震荡。device填cpu或0有显卡就填显卡编号。训练开始后runs/plate/yolov8n_plate目录下会生成results.csv和损失曲线图。重点看train/box_loss和val/box_loss是否同步下降如果训练损失降但验证损失升说明过拟合了需要加数据增强或减少 epochs。3.2 数据增强和超参数哪些该动、哪些别乱动YOLOv8 默认开启 mosaic、HSV 增强和随机翻转。车牌检测里水平翻转要谨慎因为翻转后车牌字符顺序会反虽然检测框不受影响但如果你把检测和识别端到端联合训练翻转就会让识别标签出错。我一般会关掉水平翻转保留 HSV 和轻微旋转。# plate.yaml 示例 path: ./datasets/plate train: images/train val: images/val test: images/test nc: 1 names: [plate]nc是类别数车牌检测只有一类所以填 1。names写[plate]。如果数据量少于 1000 张建议把mosaic概率调到 0.5 以下否则小数据集上容易过拟合。imgsz保持 640除非你的车牌在图中占比特别小那可以提到 1280但 CPU 训练会慢到怀疑人生。3.3 验证检测效果mAP 之外还要看什么训练完用model.val()跑验证集输出 mAP50、mAP50-95 和每类指标。车牌检测里 mAP50 到 0.9 以上通常就够用了但别只看这个数。把验证集的预测结果可视化出来看看有没有漏检小车牌、误检类似车牌的广告牌。常见做法是写个脚本批量推理验证集把框画到图上保存肉眼过一遍。from ultralytics import YOLO import cv2 model YOLO(runs/plate/yolov8n_plate/weights/best.pt) results model(test_images, saveTrue, conf0.25) for r in results: boxes r.boxes print(f检测到 {len(boxes)} 个车牌)conf0.25是置信度阈值低于这个值的框会被过滤。如果发现漏检先把 conf 降到 0.1 看看是不是阈值太高如果误检多就提高到 0.4 以上。保存的图片在runs/detect/exp下逐张检查。4. LPRNet 训练与联合推理字符集、CTC 和裁剪对齐4.1 LPRNet 网络结构为什么它比 CRNN 更适合车牌LPRNet 的核心是用全卷积网络提取特征然后接一个全局平均池化最后用 1×1 卷积输出字符类别的概率序列。它没有 RNN所以推理速度快适合部署到边缘设备。对于车牌这种字符排列规整、长度有限的场景LPRNet 的 CTC 损失能直接处理不定长序列不需要字符分割。实现时输入是 3×48×168 的车牌图经过几个卷积块后变成 1×18×68 的特征图再通过 1×1 卷积变成 1×18×68×68 的输出其中 68 是字符集大小。CTC 解码时取每个时间步的最大概率字符去掉重复和空白符得到最终字符串。import torch import torch.nn as nn class LPRNet(nn.Module): def __init__(self, num_classes): super().__init__() self.backbone nn.Sequential( nn.Conv2d(3, 64, 3, 1, 1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(64, 128, 3, 1, 1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(128, 256, 3, 1, 1), nn.BatchNorm2d(256), nn.ReLU(), nn.Conv2d(256, 256, 3, 1, 1), nn.BatchNorm2d(256), nn.ReLU(), nn.MaxPool2d((2, 1)), ) self.head nn.Conv2d(256, num_classes, 1, 1, 0) def forward(self, x): x self.backbone(x) x self.head(x) x x.mean(dim2) # 沿高度方向池化 return xnum_classes等于字符集大小加 1CTC 空白符。mean(dim2)把高度维压掉保留宽度作为时间步。训练时用nn.CTCLoss输入 log_softmax 后的序列和标签。4.2 字符集和标签编码别让“京”和“津”打架中国车牌字符集包括 31 个省份简称、26 个字母I 和 O 通常不用、10 个数字加上特殊字符总共 68 个左右。建字符集时顺序固定比如先省份简称再字母再数字。编码时把字符串转成索引列表CTC 损失需要知道每个标签的长度。chars 京沪津渝冀晋蒙辽吉黑苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新 chars ABCDEFGHJKLMNPQRSTUVWXYZ0123456789 char_to_idx {c: i 1 for i, c in enumerate(chars)} # 0 留给 CTC 空白 def encode_label(plate_str): return [char_to_idx[c] for c in plate_str if c in char_to_idx]如果遇到字符集里没有的字符直接跳过或替换成空白。训练时targets是拼接后的索引序列target_lengths是每个样本的字符数。input_lengths是 LPRNet 输出的时间步数通常是 18 或 24。4.3 联合推理YOLOv8 裁剪加 LPRNet 识别的完整脚本训练好两个模型后推理流程是YOLOv8 检测车牌框按框裁剪并缩放到 LPRNet 输入尺寸LPRNet 输出字符序列最后把结果画回原图。import cv2 import torch import numpy as np from ultralytics import YOLO det_model YOLO(runs/plate/yolov8n_plate/weights/best.pt) lpr_model LPRNet(num_classeslen(chars) 1) lpr_model.load_state_dict(torch.load(lprnet_best.pth, map_locationcpu)) lpr_model.eval() img cv2.imread(test.jpg) results det_model(img, conf0.3) for r in results: for box in r.boxes: x1, y1, x2, y2 map(int, box.xyxy[0]) plate_img img[y1:y2, x1:x2] plate_img cv2.resize(plate_img, (168, 48)) plate_img plate_img.astype(np.float32) / 255.0 plate_img np.transpose(plate_img, (2, 0, 1)) tensor torch.from_numpy(plate_img).unsqueeze(0) with torch.no_grad(): pred lpr_model(tensor) pred pred.argmax(dim1).squeeze().numpy() # CTC 解码去重、去空白 decoded [] prev -1 for idx in pred: if idx ! prev and idx ! 0: decoded.append(chars[idx - 1]) prev idx plate_text .join(decoded) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, plate_text, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imwrite(result.jpg, img)裁剪时注意框的边界不要越界x1, y1要保证大于等于 0x2, y2小于图像宽高。缩放用cv2.resize直接拉到 168×48如果原始车牌宽高比差异大可以保持比例缩放后填充灰边。CTC 解码时prev记录上一个索引跳过重复和空白符。5. 避坑与排查车牌识别系统最常见的 5 个翻车现场5.1 检测框偏了导致识别全错现象YOLOv8 检测框看起来还行但 LPRNet 输出乱码。原因裁剪时框的坐标有偏移或者缩放时把车牌拉变形了。解决把裁剪图保存下来肉眼检查确认车牌完整且没有严重变形。如果框偏检查 YOLO 标签的归一化坐标是否正确特别是多边形转矩形时有没有取错极值。5.2 字符集顺序不一致导致训练和推理对不上现象训练时 loss 正常下降推理时输出全是无意义字符。原因训练用的chars和推理用的chars顺序不同索引映射错位。解决把字符集写在一个单独的 Python 文件里训练和推理都 import 同一个变量不要手抄。5.3 CPU 训练 YOLOv8 内存爆掉现象训练几轮后进程被 kill报内存不足。原因workers设太大或者batch超过内存承受能力。解决workers设为 0 或 1batch降到 4imgsz从 640 降到 416 先跑通再逐步加回去。5.4 LPRNet 过拟合训练集准验证集崩现象训练集识别率 99%验证集只有 60%。原因数据量太少或者字符集不均衡。解决加数据增强随机亮度、对比度、轻微旋转对低频字符过采样或者用预训练权重初始化 backbone。5.5 推理速度慢到无法接受现象单张图推理要好几秒。原因CPU 上跑 YOLOv8 加 LPRNet或者输入尺寸太大。解决把 YOLOv8 换成yolov8nLPRNet 输入降到 94×24开启torch.set_num_threads(4)或者用 ONNX Runtime 加速。如果部署到 RK3588 这类板子需要先转 ONNX 再转 RKNN转换时注意算子支持情况。6. 把识别结果用起来置信度过滤和批量测试的小技巧训练完模型只是第一步真正让系统可用还得在推理阶段加一层“后悔药”。我一般会在 LPRNet 输出后计算每个字符的平均置信度低于阈值的直接标记为“不确定”而不是硬输出一个错误车牌。具体做法是在 CTC 解码时保留每个时间步的最大概率值解码后取对应字符的概率平均。with torch.no_grad(): pred lpr_model(tensor) probs torch.softmax(pred, dim1) max_probs, indices probs.max(dim1) indices indices.squeeze().numpy() max_probs max_probs.squeeze().numpy() decoded [] confidences [] prev -1 for idx, prob in zip(indices, max_probs): if idx ! prev and idx ! 0: decoded.append(chars[idx - 1]) confidences.append(prob) prev idx plate_text .join(decoded) avg_conf sum(confidences) / len(confidences) if confidences else 0 if avg_conf 0.7: plate_text 低置信度: plate_textavg_conf低于 0.7 时说明模型对这次识别不太确定可能是模糊、遮挡或光照问题。你可以把这类结果单独存下来人工复核后再决定是否加入训练集。这个习惯能帮你持续发现模型的薄弱场景。另一个技巧是批量测试时按文件名排序输出结果方便和标注对比。我习惯用glob遍历测试图把检测框、识别文本和置信度写进一个 CSV跑完直接看哪些图错了。错得多的场景往往是夜间、逆光或车牌倾斜针对性地补这类数据比盲目加 epochs 有效得多。最后说个血泪教训别在训练中途频繁改字符集或数据格式每次改动都重新从预训练权重开始训否则 loss 曲线会教你做人。车牌识别系统看着简单但检测和识别之间的那层裁剪对齐才是决定最终效果的关键。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网