简介:本资源是一套基于 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 torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cpu # 安装 ultralytics,这是 YOLOv8 的官方库 pip install ultralytics==8.0.200 # 安装 LPRNet 需要的依赖 pip install opencv-python==4.8.1.78 pip install numpy==1.24.3 pip install pillow==10.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_ok=True) for json_file in Path(json_dir).glob('*.json'): with open(json_file, 'r', encoding='utf-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.yaml:
path: ./dataset train: images/train val: images/val nc: 1 names: ['plate']然后启动训练:
yolo detect train \ data=./data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ device=cpu参数逐个解释:model=yolov8n.pt用 nano 版本,参数量最小,CPU 也能跑。epochs=100是上限,配合patience=20表示 20 轮验证集指标不提升就早停。imgsz=640是输入分辨率,车牌在图中占比小的话可以提到 1280,但 CPU 训练会非常慢。batch=16在 CPU 上可能内存不够,改成 8 或 4。lr0=0.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 \ model=runs/detect/train/weights/best.pt \ data=./data.yaml \ imgsz=640 \ device=cpu验证完可以导出 ONNX 格式,方便后续部署到 RK3588 或 Hi3516CV610 这类边缘设备:
yolo export \ model=runs/detect/train/weights/best.pt \ format=onnx \ imgsz=640 \ simplify=Truesimplify=True会做算子融合,减少推理时的计算量。导出的 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, key=cv2.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]], dtype=np.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), dtype=np.float32) s = pts.sum(axis=1) rect[0] = pts[np.argmin(s)] rect[2] = pts[np.argmax(s)] diff = np.diff(pts, axis=1) 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 Loss:
import torch import torch.nn as nn class LPRNetLoss(nn.Module): def __init__(self, num_classes): super().__init__() self.ctc = nn.CTCLoss(blank=num_classes - 1, reduction='mean') 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, dim=2) batch_size = logits.size(1) input_lengths = torch.full((batch_size,), logits.size(0), dtype=torch.long) loss = self.ctc(log_probs, targets, input_lengths, target_lengths) return loss关键参数是blank=num_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_classes=len(chars)) lprnet.load_state_dict(torch.load('lprnet_best.pth', map_location='cpu')) lprnet.eval() def recognize_plate(image_path): # 第一步:YOLOv8 检测车牌 results = detector(image_path, imgsz=640, conf=0.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(dim=1).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 贪心解码。conf=0.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 毫秒。如果图片分辨率是 4K,YOLOv8 的输入 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 也导出为 ONNX:
import 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_version=11 )然后用 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_version=11兼容性最好,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 小时的无用训练。
希望帮到你。
本文还有配套的精品资源,点击获取