简介本资源是一套基于 YOLOv8 与 LPRNet 的车牌识别系统完整项目面向计算机、人工智能等专业需要完成毕业设计、期末大作业或课程设计的学生也适合希望入门目标检测与车牌字符识别的开发者。项目将 YOLOv8 用于车牌定位、LPRNet 用于字符识别代码含详细注释新手也能理解部署后即可运行。压缩包共 60 个文件约 31.36MB包含 13 个 Python 源码文件、3 个 pt 模型权重、1 个 pth 权重、3 个 Vue 前端文件以及 json、yaml、Dockerfile、md 等配置与说明文档覆盖 Flask 后端、模型训练、数据处理与前端界面等模块。目前已有 232 人学习下载。项目经过严格调试功能完善、界面美观、操作简单可直接作为毕设或课程设计使用具有较高的实际应用与参考价值。1. 车牌识别系统从 YOLOv8 检测到 LPRNet 识别的完整落地路径很多同学做毕业设计时一上来就想端到端直接识别车牌上的字符结果发现模型根本收敛不了。车牌识别这件事本质上要拆成两个独立子任务先检测出车牌在画面中的位置再对裁剪出的车牌区域做字符序列识别。YOLOv8 负责第一步LPRNet 负责第二步这个组合在中文车牌场景下是目前最成熟、最容易复现的方案之一。整套系统用 Python 实现训练好的模型可以直接推理不需要你自己从零标注几万张图。适合做计算机毕业设计、想快速跑通一个完整视觉项目的同学也适合已经会 YOLOv8 检测但不知道怎么接识别模块的开发者。下面我会把数据准备、检测训练、识别训练、推理串联、部署踩坑全部讲清楚你照着做就能跑通。2. 为什么选 YOLOv8 LPRNet 而不是端到端方案2.1 两阶段方案在中文车牌上的实际优势中文车牌和英文车牌最大的区别在于字符集。蓝牌是 7 位新能源牌是 8 位包含 31 个省份简称汉字、26 个字母I 和 O 通常不用、10 个数字。如果用一个端到端模型直接从整图回归出字符序列模型需要同时学习定位和分类训练难度大收敛慢而且换一个拍摄角度或光照条件就容易崩。拆成两阶段之后YOLOv8 只需要学“车牌在哪里”LPRNet 只需要学“车牌上写了什么”。两个任务的输入分布更集中训练数据需求也更低。实际项目中YOLOv8n 在 2000 张车牌图上就能达到不错的检测精度LPRNet 在 5000 张裁剪车牌上就能稳定识别。这个数据量对毕业设计来说完全可控。另一个关键点是可调试性。端到端模型出错时你很难判断是定位偏了还是字符分类错了。两阶段方案里检测框偏了可以单独调 YOLOv8 的 anchor 和 NMS 参数字符识别错了可以单独看 LPRNet 的 CTC 解码结果。这种模块化带来的排查便利在赶毕设进度的时候就是救命稻草。2.2 LPRNet 的结构特点与 CTC 解码逻辑LPRNet 的核心设计思路是轻量级卷积骨干加 CTC 损失。它没有用 RNN 做序列建模而是用全卷积结构直接输出字符概率序列再通过 CTC 解码得到最终字符串。这个设计让模型参数量很小推理速度快适合部署在边缘设备上。具体来说输入是一张 94x24 的 RGB 车牌图经过几个卷积块和最大池化后特征图尺寸变成 18x4 左右然后通过 1x1 卷积把通道数映射到字符集大小加一个空白符。CTC 的作用是解决输入特征序列长度和输出字符序列长度不对齐的问题。比如车牌有 7 个字符但特征序列有 18 个时间步CTC 会自动学习哪些时间步对应空白、哪些对应字符、哪些重复需要合并。这里有个容易翻车的点CTC 解码时如果遇到连续相同字符必须中间有空白符才能区分。比如“京A88888”里有五个 8如果特征序列里连续五个时间步都输出 8 而没有空白分隔CTC 会合并成一个 8。所以训练时空白符的引入和特征序列长度的设计很关键。LPRNet 原论文里特征序列长度是 18对 7 位车牌够用但如果你要支持 8 位新能源牌建议把输入宽度从 94 增加到 120 左右让特征序列长度达到 22 以上。2.3 环境配置Ubuntu 20.04 下 CPU 版本也能跑很多同学没有独立显卡担心跑不了 YOLOv8。实际上 YOLOv8 的推理和训练都支持 CPU 模式只是训练速度慢一些。Ubuntu 20.04 下配置环境的步骤如下# 创建虚拟环境避免污染系统 Python python3 -m venv plate_env source plate_env/bin/activate # 安装 PyTorch CPU 版本注意版本匹配 pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cpu # 安装 ultralytics这是 YOLOv8 的官方库 pip install ultralytics8.0.200 # 安装 LPRNet 需要的依赖 pip install opencv-python4.8.1.78 pip install numpy1.24.3 pip install pillow10.0.1这段命令的逻辑是先隔离环境再装 PyTorch 的 CPU 版本然后装 ultralytics 和图像处理库。参数上注意 torch 和 torchvision 版本要对应ultralytics 8.0.200 是经过验证比较稳定的版本。如果你用的是 GTX 1660Ti 这类显卡把第一行 pip install 的 index-url 换成 CUDA 版本即可训练速度会快 10 倍以上。提示Ubuntu 20.04 自带的 Python 是 3.8建议用 3.9 或 3.10兼容性更好。用python3 --version确认版本。3. 车牌检测YOLOv8 数据准备与训练参数3.1 用 Labelme 标注后转 YOLO 格式的完整脚本YOLOv8 需要的数据格式是每张图对应一个 txt 文件每行是类别 x_center y_center width height坐标都归一化到 0 到 1 之间。如果你用 Labelme 标注得到的是 JSON 文件需要转换。下面是转换脚本import json import os from pathlib import Path def labelme_to_yolo(json_dir, output_dir, class_names): 将 Labelme 标注的 JSON 转为 YOLO 格式的 txt json_dir: JSON 文件所在目录 output_dir: 输出 txt 的目录 class_names: 类别名称列表如 [plate] os.makedirs(output_dir, exist_okTrue) for json_file in Path(json_dir).glob(*.json): with open(json_file, 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_names: continue cls_id class_names.index(label) # Labelme 给的是多边形点取外接矩形 points shape[points] xs [p[0] for p in points] ys [p[1] for p in points] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) # 计算中心点和宽高并归一化 x_center (x_min x_max) / 2.0 / img_w y_center (y_min y_max) / 2.0 / img_h width (x_max - x_min) / img_w height (y_max - y_min) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) # 写入同名 txt txt_path os.path.join(output_dir, json_file.stem .txt) with open(txt_path, w) as f: f.write(\n.join(lines)) # 调用示例 labelme_to_yolo( json_dir./raw_data/jsons, output_dir./raw_data/labels, class_names[plate] )这段代码的关键逻辑是读取 JSON 里的多边形点取外接矩形然后按图片宽高归一化。参数上class_names只放[plate]因为车牌检测只有一个类别。如果你的数据集里还有“车牌框”和“车牌”两个类别需要改成两个。转换完成后按照 YOLOv8 要求的目录结构组织dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/图片和标签文件名要一一对应比如images/train/001.jpg对应labels/train/001.txt。划分比例一般 8:2 或 9:1毕业设计数据量不大的话建议 8:2验证集至少留 200 张。3.2 YOLOv8 训练参数怎么设从 epochs 到 imgsz数据准备好之后写一个data.yamlpath: ./dataset train: images/train val: images/val nc: 1 names: [plate]然后启动训练yolo detect train \ data./data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ devicecpu参数逐个解释modelyolov8n.pt用 nano 版本参数量最小CPU 也能跑。epochs100是上限配合patience20表示 20 轮验证集指标不提升就早停。imgsz640是输入分辨率车牌在图中占比小的话可以提到 1280但 CPU 训练会非常慢。batch16在 CPU 上可能内存不够改成 8 或 4。lr00.01是初始学习率YOLOv8 默认用 SGD 优化器这个值比较稳。训练过程中重点看mAP50和mAP50-95两个指标。车牌检测场景下mAP50 到 0.95 以上基本可用mAP50-95 到 0.7 以上算不错。如果 mAP50 卡在 0.8 上不去优先检查标注框有没有漏标或错标而不是调参。3.3 训练完的模型怎么验证和导出训练结束后权重保存在runs/detect/train/weights/best.pt。用下面的命令在验证集上跑一遍yolo detect val \ modelruns/detect/train/weights/best.pt \ data./data.yaml \ imgsz640 \ devicecpu验证完可以导出 ONNX 格式方便后续部署到 RK3588 或 Hi3516CV610 这类边缘设备yolo export \ modelruns/detect/train/weights/best.pt \ formatonnx \ imgsz640 \ simplifyTruesimplifyTrue会做算子融合减少推理时的计算量。导出的 ONNX 模型可以用 Netron 打开看网络结构确认输入输出节点名称后面部署时要用到。注意CPU 训练 100 轮在 2000 张图上大约需要 6 到 8 小时建议晚上跑。如果有 GTX 1660Ti同样数据量大约 40 分钟。4. LPRNet 识别从车牌裁剪到字符序列输出4.1 车牌裁剪与预处理对齐比裁剪更重要YOLOv8 检测出的车牌框通常不是水平的有倾斜角度。直接裁剪送入 LPRNet 会导致字符变形识别率大幅下降。常见做法是用透视变换把车牌矫正到水平import cv2 import numpy as np def crop_plate(image, box): image: 原始 BGR 图像 box: YOLOv8 输出的 [x1, y1, x2, y2] 返回矫正后的车牌图尺寸 94x24 x1, y1, x2, y2 map(int, box) plate image[y1:y2, x1:x2] # 转灰度后做边缘检测找车牌四个角点 gray cv2.cvtColor(plate, cv2.COLOR_BGR2GRAY) edges cv2.Canny(gray, 50, 150) contours, _ cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: # 取最大轮廓近似四边形 cnt max(contours, keycv2.contourArea) peri cv2.arcLength(cnt, True) approx cv2.approxPolyDP(cnt, 0.02 * peri, True) if len(approx) 4: # 透视变换到标准尺寸 pts approx.reshape(4, 2).astype(np.float32) # 按左上、右上、右下、左下排序 rect order_points(pts) dst np.array([[0, 0], [94, 0], [94, 24], [0, 24]], dtypenp.float32) M cv2.getPerspectiveTransform(rect, dst) plate cv2.warpPerspective(plate, M, (94, 24)) # 统一 resize 并归一化 plate cv2.resize(plate, (94, 24)) plate plate.astype(np.float32) / 255.0 return plate def order_points(pts): 将四个点按左上、右上、右下、左下排序 rect np.zeros((4, 2), dtypenp.float32) s pts.sum(axis1) rect[0] pts[np.argmin(s)] rect[2] pts[np.argmax(s)] diff np.diff(pts, axis1) rect[1] pts[np.argmin(diff)] rect[3] pts[np.argmax(diff)] return rect这段代码的逻辑是先裁剪再用 Canny 边缘检测找车牌轮廓如果找到四边形就做透视变换矫正最后统一到 94x24 并归一化。参数上94x24是 LPRNet 的标准输入尺寸如果你要支持新能源 8 位车牌改成120x24。0.02 * peri是轮廓近似精度值越小越贴近原始轮廓但可能不是四边形。4.2 LPRNet 训练字符集定义与 CTC 损失配置LPRNet 的字符集需要根据你的数据集来定。中文蓝牌字符集包括# 省份简称 provinces [京, 津, 冀, 晋, 蒙, 辽, 吉, 黑, 沪, 苏, 浙, 皖, 闽, 赣, 鲁, 豫, 鄂, 湘, 粤, 桂, 琼, 渝, 川, 贵, 云, 藏, 陕, 甘, 青, 宁, 新] # 字母和数字 letters [chr(i) for i in range(65, 91) if chr(i) not in [I, O]] digits [str(i) for i in range(10)] # 空白符用于 CTC chars provinces letters digits [-] char_to_idx {c: i for i, c in enumerate(chars)} idx_to_char {i: c for c, i in char_to_idx.items()}训练时的损失函数用 CTC Lossimport torch import torch.nn as nn class LPRNetLoss(nn.Module): def __init__(self, num_classes): super().__init__() self.ctc nn.CTCLoss(blanknum_classes - 1, reductionmean) def forward(self, logits, targets, target_lengths): # logits 形状: (batch, num_classes, time_steps) # CTC 需要 (time_steps, batch, num_classes) logits logits.permute(2, 0, 1) log_probs torch.log_softmax(logits, dim2) batch_size logits.size(1) input_lengths torch.full((batch_size,), logits.size(0), dtypetorch.long) loss self.ctc(log_probs, targets, input_lengths, target_lengths) return loss关键参数是blanknum_classes - 1把字符集最后一个索引作为空白符。permute(2, 0, 1)是因为 PyTorch 的 CTCLoss 要求时间步在第一维。input_lengths全部设为特征序列长度因为 LPRNet 是全卷积结构每个样本的时间步相同。训练时 batch size 建议 32 到 64学习率用 0.001 配合 Adam 优化器。训练 50 到 80 轮观察 CTC Loss 是否降到 0.1 以下。如果 Loss 震荡不降检查字符集里有没有重复字符或者空白符索引是否设置正确。4.3 推理串联YOLOv8 检测框送入 LPRNet 的完整代码把两个模型串起来from ultralytics import YOLO import torch # 加载检测模型和识别模型 detector YOLO(runs/detect/train/weights/best.pt) lprnet LPRNet(num_classeslen(chars)) lprnet.load_state_dict(torch.load(lprnet_best.pth, map_locationcpu)) lprnet.eval() def recognize_plate(image_path): # 第一步YOLOv8 检测车牌 results detector(image_path, imgsz640, conf0.5) boxes results[0].boxes.xyxy.cpu().numpy() plates [] for box in boxes: # 第二步裁剪并预处理 plate_img crop_plate(cv2.imread(image_path), box) # 转成 tensor增加 batch 维度 plate_tensor torch.from_numpy(plate_img).permute(2, 0, 1).unsqueeze(0) # 第三步LPRNet 推理 with torch.no_grad(): logits lprnet(plate_tensor) # CTC 贪心解码 preds logits.argmax(dim1).squeeze().numpy() # 解码字符序列去掉空白和重复 plate_str prev -1 for p in preds: if p ! prev and p ! len(chars) - 1: plate_str idx_to_char[p] prev p plates.append(plate_str) return plates这段代码的逻辑是YOLOv8 检测出所有车牌框逐个裁剪预处理后送入 LPRNet得到字符概率序列后用 CTC 贪心解码。conf0.5是检测置信度阈值低于这个值的框丢弃。解码时p ! prev去重p ! len(chars) - 1去空白。注意prev初始化为 -1避免第一个字符被误去重。提示如果一张图里有多个车牌boxes会有多行循环处理即可。实际测试时建议先用单张图验证再批量跑。5. 避坑与排查车牌识别系统最常见的 5 个翻车点5.1 检测框偏大导致识别率骤降现象YOLOv8 检测出的车牌框比实际车牌大一圈裁剪后包含大量背景LPRNet 识别结果乱码。原因训练数据标注时框画得太松或者 YOLOv8 的box损失权重不够模型倾向于输出大框。解决重新检查标注框紧贴车牌边缘。训练时把box损失权重从默认 7.5 提高到 10或者在推理时对检测框做内缩比如宽高各缩 10%。内缩代码x1 w*0.05; x2 - w*0.05; y1 h*0.05; y2 - h*0.05。5.2 CTC 解码输出重复字符现象识别结果里出现“京A88888”变成“京A8”或“京A888888”的情况。原因CTC 解码时连续相同字符没有空白分隔或者特征序列长度不够导致多个字符挤在一个时间步。解决检查训练数据里是否有连续相同字符的样本增加这类样本的比例。把 LPRNet 输入宽度从 94 增加到 120让特征序列长度从 18 增加到 22 以上。解码时用 Beam Search 替代贪心解码能缓解但增加计算量。5.3 CPU 推理速度太慢现象单张图推理超过 2 秒批量测试时等得让人崩溃。原因YOLOv8n 在 CPU 上单张推理约 200 到 300 毫秒LPRNet 约 50 毫秒加上预处理和后处理累计超过 500 毫秒。如果图片分辨率是 4KYOLOv8 的输入 resize 会额外耗时。解决把 YOLOv8 导出为 ONNX 并用 onnxruntime 推理CPU 上能快 30% 左右。或者先把图片缩放到 1280 宽再送入检测减少 resize 耗时。如果对实时性要求高建议用 RK3588 这类带 NPU 的板子YOLOv8n 能跑到 30 FPS 以上。5.4 新能源车牌识别率低现象蓝牌识别正常但绿牌新能源车牌经常少识别一位或识别错。原因新能源车牌是 8 位比蓝牌多一位LPRNet 的特征序列长度 18 对 8 位车牌偏短CTC 解码时最后一个字符容易被截断。解决把 LPRNet 输入宽度改成 120特征序列长度增加到 22。训练数据里增加新能源车牌样本至少占总量的 30%。字符集里确认没有遗漏新能源车牌特有的字母组合。5.5 模型加载报错KeyError 或 size mismatch现象加载训练好的 LPRNet 权重时提示KeyError: module.conv1.weight或size mismatch for fc.weight。原因保存权重时用了nn.DataParallel加载时没有对应前缀。或者字符集大小变了最后一层全连接层维度不匹配。解决保存时用torch.save(model.module.state_dict(), path)去掉module.前缀。加载时如果字符集变了重新初始化最后一层lprnet.fc nn.Linear(in_features, len(chars))然后只加载前面层的权重。6. 进阶技巧用 ONNX Runtime 加速推理并验证端到端精度6.1 导出 ONNX 并用 onnxruntime 推理把 LPRNet 也导出为 ONNXimport torch.onnx dummy_input torch.randn(1, 3, 24, 94) torch.onnx.export( lprnet, dummy_input, lprnet.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}}, opset_version11 )然后用 onnxruntime 推理import onnxruntime as ort session ort.InferenceSession(lprnet.onnx, providers[CPUExecutionProvider]) def infer_onnx(plate_tensor): # plate_tensor 形状 (1, 3, 24, 94) inputs {session.get_inputs()[0].name: plate_tensor.numpy()} outputs session.run(None, inputs) return outputs[0]ONNX Runtime 在 CPU 上比 PyTorch 原生推理快 20% 到 40%而且内存占用更低。opset_version11兼容性最好dynamic_axes让 batch 维度可变方便批量推理。6.2 端到端精度验证用 100 张测试图算准确率准备 100 张没参与训练的图片按下面的脚本算端到端准确率correct 0 total 0 for img_path, true_label in test_set: pred recognize_plate(img_path) if pred and pred[0] true_label: correct 1 total 1 print(f端到端准确率: {correct / total * 100:.2f}%)如果准确率低于 85%按这个顺序排查先单独看检测框准不准再单独看裁剪后的车牌图 LPRNet 识别对不对。检测准但识别错问题在 LPRNet 或预处理检测就不准回头调 YOLOv8。6.3 我踩过的坑和固定习惯做这套系统最大的教训是不要一上来就调模型结构。我一开始觉得 LPRNet 识别率低改了 backbone、加了注意力模块折腾一周发现是裁剪时透视变换的角点排序错了导致车牌上下颠倒。后来我固定了一个习惯任何识别错误先保存中间图——检测框画出来、裁剪图存下来、LPRNet 的 logits 打印出来。这三张图一看问题基本就定位了。另一个习惯是训练前先跑 10 张图的过拟合测试。YOLOv8 和 LPRNet 各拿 10 张图训练 100 轮如果 Loss 降不到接近 0说明代码有 bug不用继续跑大数据集。这个习惯帮我省了至少 20 小时的无用训练。希望帮到你。本文还有配套的精品资源点击获取