news 2026/9/27 1:30:24

基于YOLOv8和LPRNet的车牌识别系统:从跑通到改进的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于YOLOv8和LPRNet的车牌识别系统:从跑通到改进的完整指南

简介:这份资源是面向计算机、数学、电子信息等专业学生的毕业设计与课程设计参考资料,核心为基于 YOLOv8 与 LPRNet 的车牌识别系统,提供完整 Python 源码与训练好的模型权重,下载后可直接运行调试。项目采用 Flask 搭建后端服务,前端使用 Vue 构建可视化界面,并配套 Dockerfile 与依赖清单,便于环境部署与二次开发。压缩包共 60 个文件,约 31.35MB,涵盖 13 个 py 脚本、3 个 pt 模型文件、1 个 pth 权重、3 个 json 配置、3 个 vue 组件以及 jpg 示例图片、md 说明文档等,覆盖数据处理、模型训练、推理部署与前端展示全流程。目前已有 606 人学习下载,适合作为毕设选题、期末大作业或算法入门练手项目,读者可借此理解车牌检测与字符识别的两阶段实现思路,掌握数据集标注、模型训练与 Web 端集成的完整链路,并在此基础上自行扩展功能。

1. 车牌识别这套源码,为什么我建议先跑通再谈改进

车牌识别这个方向,每年毕业设计都有人做,但真正能一次跑通的没几个。我拿到这份「基于 YOLOv8 和 LPRNet 的车牌识别系统 python 源码 + 模型」时,第一反应不是看代码写得多漂亮,而是先确认两件事:检测和识别是不是真的解耦成两个独立模型,以及模型权重是不是随包给的。答案是肯定的——YOLOv8 负责把车牌从整张图里框出来,LPRNet 负责把框出来的车牌小图转成字符序列。这个两阶段结构是目前工业界车牌识别最稳的路线,也是最适合拿来改毕业设计的骨架。

它解决的核心问题是:你不需要从零训练一个端到端模型,也不需要自己标注几万张车牌数据。包里已经带了训练好的检测权重和识别权重,你拿到手就能对一张普通街景图跑出「京A12345」这样的结果。适合谁?适合正在做毕业设计、需要快速出 demo 的本科生,也适合想拿一个干净 baseline 做二次开发的研究生。但前提是,你得先把环境跑通,而不是一上来就改网络结构。

2. 拆开这个 zip:YOLOv8 检测 + LPRNet 识别的两阶段结构

2.1 为什么是两阶段而不是端到端

车牌识别看起来是一个「图进字出」的任务,但实际工程里几乎没人用纯端到端。原因很直接:车牌在整张图里占比很小,端到端模型既要定位又要识别,训练时梯度会被背景淹没,收敛慢且容易过拟合。两阶段的好处是每一段只做一件事——YOLOv8 只管「车牌在哪」,LPRNet 只管「车牌上写了什么」。这样你调检测的时候不影响识别,调识别的时候不影响检测,排查问题的时候能快速定位是哪一段出了错。

YOLOv8 在这份源码里用的是 nano 或 small 级别的权重,输入尺寸通常是 640×640。检测输出的是车牌的边界框坐标,代码里会做一个 padding 扩展,把框稍微放大一点再裁剪,目的是让 LPRNet 能看到完整的字符边缘。这个 padding 比例一般设在 0.1 到 0.15 之间,太小会切掉字符,太大会引入过多背景。

LPRNet 是一个轻量级的字符识别网络,核心是 CNN + CTC 损失。它不需要对每个字符做分割,直接输出字符序列的概率分布,再用 CTC 解码成最终字符串。这个设计对车牌这种定长但字符间距不固定的场景非常友好。源码里 LPRNet 的输入尺寸通常是 94×24 或 128×32,具体看训练时的配置。

2.2 源码目录里每个文件干什么

拿到包之后先别急着跑,花五分钟把目录结构看清楚,后面能省很多时间。典型的目录长这样:

plate_recognition/ ├── detect/ │ ├── yolov8_plate.pt # YOLOv8 车牌检测权重 │ └── detect.py # 检测推理脚本 ├── recognize/ │ ├── lprnet_best.pth # LPRNet 识别权重 │ ├── lprnet.py # 网络定义 │ └── recognize.py # 识别推理脚本 ├── utils/ │ ├── plate_chars.py # 字符集定义 │ └── image_process.py # 裁剪、归一化等预处理 ├── demo.jpg # 示例图片 └── main.py # 串联检测和识别的入口

plate_chars.py这个文件特别重要,它定义了 LPRNet 能识别的字符集。中国大陆车牌通常包含省份简称、字母、数字和特殊字符,一共 68 个左右。如果你后面要换自己的权重,字符集必须和训练时完全一致,否则解码出来的就是乱码。

main.py是整条流水线的入口,逻辑是:读图 → YOLOv8 检测 → 裁剪车牌区域 → 预处理 → LPRNet 识别 → 输出结果。你第一次跑通就用这个文件,不要一上来就改detect.py或recognize.py。

2.3 环境依赖和版本锁定

这份源码对版本比较敏感,尤其是 ultralytics 和 PyTorch 的搭配。我建议直接用 Python 3.8 或 3.9,不要用 3.11 以上,否则某些依赖会编译失败。下面是我验证过能跑通的依赖清单:

# 创建虚拟环境,避免污染系统 Python python -m venv venv_plate source venv_plate/bin/activate # Windows 用 venv_plate\Scripts\activate # 安装 PyTorch,CPU 版本就够跑推理 pip install torch==1.13.1 torchvision==0.14.1 --index-url https://download.pytorch.org/whl/cpu # 安装 ultralytics 和其他依赖 pip install ultralytics==8.0.145 opencv-python==4.8.0.74 numpy==1.24.3 pillow==9.5.0

这里锁ultralytics==8.0.145是有原因的:新版本改了 API,YOLO类的某些参数名变了,源码里如果写的是旧参数名,跑起来会报TypeError。opencv-python锁 4.8 是因为 4.9 之后某些图像解码行为有变化,可能导致裁剪区域偏移。这些版本号不是随便写的,是我踩过坑之后回退确认的。

提示:如果你用的是 Windows,pip install之前先确认 Visual C++ Redistributable 已经装了,否则 opencv 可能导入失败。

3. 从一张图到一串字符:推理流水线的代码级拆解

3.1 检测阶段:YOLOv8 怎么框出车牌

检测这一步的代码不复杂,但有几个参数直接决定后面识别能不能做。先看核心代码:

from ultralytics import YOLO import cv2 # 加载车牌检测模型 model = YOLO("detect/yolov8_plate.pt") # 读图并推理 img = cv2.imread("demo.jpg") results = model(img, conf=0.25, iou=0.45, imgsz=640) # 提取边界框 boxes = results[0].boxes for box in boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() conf = box.conf[0].item() cls = int(box.cls[0].item()) print(f"检测到车牌: 坐标=({x1:.0f},{y1:.0f},{x2:.0f},{y2:.0f}), 置信度={conf:.2f}")

conf=0.25是置信度阈值,低于这个值的框会被丢弃。车牌检测场景下不建议设太高,因为远距离或模糊车牌的置信度天然偏低,设 0.5 以上容易漏检。iou=0.45是 NMS 的 IoU 阈值,控制重叠框的合并程度。imgsz=640是推理分辨率,如果你图片里车牌特别小,可以调到 1280,但速度会慢一倍左右。

检测完之后不要直接把x1,y1,x2,y2拿去裁剪,先做一个 padding 扩展:

import numpy as np def expand_box(x1, y1, x2, y2, w, h, pad_ratio=0.12): """按比例扩展边界框,避免切掉字符边缘""" bw, bh = x2 - x1, y2 - y1 pad_w, pad_h = bw * pad_ratio, bh * pad_ratio nx1 = max(0, int(x1 - pad_w)) ny1 = max(0, int(y1 - pad_h)) nx2 = min(w, int(x2 + pad_w)) ny2 = min(h, int(y2 + pad_h)) return nx1, ny1, nx2, ny2

pad_ratio=0.12是我试出来的经验值。太小(比如 0.05)时,如果检测框本身偏紧,字符的上下边缘会被切掉,LPRNet 识别出来会缺字。太大(比如 0.3)时,裁剪区域会包含太多车身或背景,LPRNet 的注意力被分散,置信度下降。0.1 到 0.15 之间是比较稳的区间。

3.2 识别阶段:LPRNet 的输入预处理和 CTC 解码

裁剪出来的车牌图不能直接丢给 LPRNet,要先做尺寸归一化和归一化。LPRNet 对输入尺寸有固定要求,源码里一般是 94×24:

import cv2 import numpy as np import torch def preprocess_plate(plate_img, target_size=(94, 24)): """将裁剪的车牌图缩放到 LPRNet 输入尺寸并归一化""" img = cv2.resize(plate_img, target_size, interpolation=cv2.INTER_LINEAR) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = img.astype(np.float32) / 255.0 img = (img - 0.5) / 0.5 # 归一化到 [-1, 1] img = np.transpose(img, (2, 0, 1)) # HWC -> CHW tensor = torch.from_numpy(img).unsqueeze(0) # 增加 batch 维度 return tensor

这里有两个容易翻车的点。第一,cv2.resize的插值方式用INTER_LINEAR就够,不要用INTER_CUBIC,后者在放大时会产生振铃效应,反而让字符边缘变模糊。第二,归一化参数(img - 0.5) / 0.5必须和训练时一致,如果你换了权重但没改归一化参数,识别结果会完全错乱。

LPRNet 的输出是一个(T, N, C)的张量,T 是时间步,N 是 batch,C 是字符集大小。CTC 解码用贪心策略就够了:

def ctc_greedy_decode(logits, chars): """CTC 贪心解码:取每个时间步最大概率的字符,去重去 blank""" indices = torch.argmax(logits, dim=2) # (T, N) indices = indices[:, 0].cpu().numpy() # 取第一个 batch result = [] prev = -1 for idx in indices: if idx != prev and idx != 0: # 0 是 blank result.append(chars[idx - 1]) prev = idx return "".join(result)

chars就是plate_chars.py里定义的字符集。注意idx - 1是因为 CTC 的 blank 占了索引 0,实际字符从 1 开始。这个偏移量如果搞错,所有识别结果都会整体错位一位。

3.3 把两段串起来:main.py 的完整调用链

理解了检测和识别各自的逻辑之后,main.py就是一条直线:

import cv2 from detect.detect import detect_plates from recognize.recognize import recognize_plate img = cv2.imread("demo.jpg") h, w = img.shape[:2] # 第一步:检测 boxes = detect_plates(img, conf=0.25, iou=0.45) # 第二步:逐个裁剪并识别 for (x1, y1, x2, y2) in boxes: nx1, ny1, nx2, ny2 = expand_box(x1, y1, x2, y2, w, h, pad_ratio=0.12) plate_img = img[ny1:ny2, nx1:nx2] text = recognize_plate(plate_img) print(f"车牌号: {text}") cv2.rectangle(img, (nx1, ny1), (nx2, ny2), (0, 255, 0), 2) cv2.putText(img, text, (nx1, ny1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imwrite("result.jpg", img)

跑完这一步,你会得到一张画了框、标了车牌号的result.jpg。如果框的位置对但字符是乱的,问题在识别段;如果框都没出来,问题在检测段。这种分段排查的思路比端到端模型省时间得多。

4. 避坑排查:跑不通时先看这五个地方

4.1 报错No module named 'ultralytics'或版本不兼容

现象:pip install ultralytics之后导入仍然报错,或者YOLO()初始化时报TypeError: __init__() got an unexpected keyword argument。

原因:ultralytics 8.x 的 API 在 8.0.100 之后有过一次调整,YOLO类的某些参数名从model改成了weights,或者predict的参数名变了。源码如果是按旧版本写的,装最新版就会挂。

解决:先pip show ultralytics看版本,如果不是 8.0.145 左右,直接pip install ultralytics==8.0.145锁死。如果还报错,把YOLO("xxx.pt")改成YOLO(model="xxx.pt")试试,这是最常见的参数名差异。

4.2 检测框位置对但识别结果是乱码

现象:result.jpg里框的位置很准,但putText出来的字符是「###」或者完全不相干的字母。

原因:九成是字符集不匹配。LPRNet 的输出索引要映射到plate_chars.py里的字符列表,如果你换了权重但没换字符集,或者字符集顺序和训练时不一致,解码出来就是乱的。

解决:打开plate_chars.py,确认字符列表的顺序和长度。中国大陆车牌通常是 68 个字符(含 blank),如果你用的是新能源车牌权重,字符集可能不一样。最稳妥的办法是找权重作者确认字符集,或者用一张已知车牌号的图反推——如果「京」被识别成「A」,说明索引偏移了。

4.3 图片里车牌很小,检测直接漏掉

现象:整张图能看到车牌,但 YOLOv8 一个框都没输出,或者只输出了置信度很低的框。

原因:YOLOv8 默认输入 640×640,如果原图是 4000×3000,车牌在缩放后可能只有几十个像素,特征太弱。

解决:两个方向。一是把imgsz调到 1280 或 1536,让车牌在输入里占更多像素;二是先对原图做裁剪,把车牌区域大致裁出来再送检测。我一般会先跑一次 640,如果没框就自动升到 1280 重跑,这个逻辑在detect.py里加个循环就行。

4.4 中文车牌省份简称识别不出来

现象:数字和字母都对,但第一个汉字(如「京」「沪」「粤」)总是错或者变成 blank。

原因:LPRNet 的训练数据里如果省份简称样本太少,模型对这类字符的响应就弱。另外,省份简称的笔画复杂,在 94×24 的输入尺寸下信息损失严重。

解决:短期办法是把 LPRNet 输入尺寸从 94×24 调到 128×32,给汉字更多像素。长期办法是拿一批带省份简称的车牌图对 LPRNet 做 fine-tune,只调最后几层,学习率设 1e-4,跑 10 个 epoch 左右就能明显改善。

4.5 CPU 推理速度太慢,一张图要好几秒

现象:在没独显的笔记本上跑,YOLOv8 推理就要 2 秒以上,加上 LPRNet 总共 3 到 4 秒。

原因:ultralytics 默认用 PyTorch 的 CPU 推理,没有做图优化。YOLOv8n 在 CPU 上理论可以做到 100ms 以内,但默认配置没启用。

解决:导出 ONNX 模型用 onnxruntime 推理,速度能快 2 到 3 倍。命令是yolo export model=yolov8_plate.pt format=onnx,然后用onnxruntime.InferenceSession加载。如果不想换推理框架,至少把torch.set_num_threads(4)加上,利用多核。

5. 换自己的数据:从标注到微调的完整路径

5.1 用 labelme 标注车牌并转 YOLO 格式

如果你要做自己场景的车牌检测(比如停车场、收费站),第一步是标注。labelme 是最顺手的工具,标完得到的是 JSON,需要转成 YOLO 的 txt 格式:

import json import os def labelme_to_yolo(json_path, output_dir, class_id=0): """将 labelme 的 JSON 标注转成 YOLO 格式的 txt""" with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) img_w = data["imageWidth"] img_h = data["imageHeight"] lines = [] for shape in data["shapes"]: points = shape["points"] xs = [p[0] for p in points] ys = [p[1] for p in points] x1, x2 = min(xs), max(xs) y1, y2 = min(ys), max(ys) # YOLO 格式:class_id cx cy w h,全部归一化到 0-1 cx = (x1 + x2) / 2 / img_w cy = (y1 + y2) / 2 / img_h bw = (x2 - x1) / img_w bh = (y2 - y1) / img_h lines.append(f"{class_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") txt_name = os.path.splitext(os.path.basename(json_path))[0] + ".txt" with open(os.path.join(output_dir, txt_name), "w") as f: f.write("\n".join(lines))

class_id=0是因为车牌检测只有一个类别。归一化的时候注意cx和cy是中心点坐标,不是左上角。这个转换脚本我一般会批量跑,把整个目录的 JSON 一次性转完。

5.2 YOLOv8 微调的关键参数

转完格式之后,写一个data.yaml:

path: ./dataset train: images/train val: images/val nc: 1 names: ["plate"]

然后启动训练:

yolo detect train model=yolov8n.pt data=data.yaml epochs=100 imgsz=640 batch=16 lr0=0.01

epochs=100对车牌这种单类别任务通常够了,如果 loss 还在降可以加到 150。batch=16是 8G 显存下的安全值,显存不够就降到 8。lr0=0.01是初始学习率,微调时如果数据量小于 2000 张,建议降到 0.001,避免把预训练权重冲掉。

训练完之后,runs/detect/train/weights/best.pt就是你的新检测权重。把它替换掉detect/yolov8_plate.pt,重新跑main.py就能看到自己场景的效果。

5.3 LPRNet 微调时字符集怎么对齐

LPRNet 的微调比 YOLOv8 麻烦一点,因为字符集必须和训练数据完全对齐。如果你只做标准蓝牌,字符集不用动;如果要加新能源绿牌或黄牌,字符集要扩充。

扩充字符集的步骤是:先统计你所有训练图片里出现过的字符,去重后排序,确保 blank 在索引 0。然后修改lprnet.py里最后一层全连接层的输出维度,改成len(chars) + 1。最后加载预训练权重时,用strict=False跳过最后一层的形状不匹配:

model = LPRNet(chars=chars) state_dict = torch.load("lprnet_best.pth", map_location="cpu") model.load_state_dict(state_dict, strict=False) # 最后一层会重新初始化

strict=False的意思是允许部分层不匹配,不匹配的层保持随机初始化。这样你既能复用前面卷积层的特征提取能力,又能让最后一层适应新的字符集。微调时学习率设 1e-4,跑 20 到 30 个 epoch,观察验证集上的准确率。

5.4 验证微调效果:别只看 loss

训练完之后不要只看 loss 曲线,要拿一批没参与训练的图做端到端测试。我一般会准备 50 张图,人工记录真实车牌号,然后跑main.py统计完全正确的比例。如果检测框准但字符错,问题在 LPRNet;如果框偏了,问题在 YOLOv8。这个分段的验证方法比看 loss 直观得多。

另外注意,车牌识别里「完全正确」的标准很严格——只要有一个字符错就算错。所以如果你的准确率是 80%,意味着 5 张里有 1 张至少错一个字符。这个指标在毕业设计里够用,但离工业级还有距离。想往上提,要么加数据,要么在 LPRNet 后面接一个语言模型做后处理,把「京A12345」这种不符合规则的组合纠正过来。

6. 把 demo 变成能演示的系统:三个我踩过的坑

第一个坑是路径问题。源码里很多地方写的是相对路径,比如"detect/yolov8_plate.pt",你在项目根目录跑没问题,但如果你在detect/目录里跑python detect.py,就会报文件找不到。我的习惯是在所有脚本开头加os.chdir(os.path.dirname(os.path.abspath(__file__))),把工作目录强制切到脚本所在目录,这样不管从哪跑都不会错。

第二个坑是 OpenCV 的imread读中文路径会返回 None。如果你的图片放在「D:/毕业设计/测试图/1.jpg」这种路径下,cv2.imread直接失败。解决办法是用cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR)代替,或者把图片路径改成全英文。这个坑我当年调了一下午才定位到,血泪经验。

第三个坑是模型权重加载时的设备不匹配。如果你在 GPU 上训练的权重,拿到 CPU 机器上加载会报RuntimeError: Attempting to deserialize object on a CUDA device。解决是在torch.load里加map_location="cpu",这个参数一定要养成习惯加上,不管当前有没有 GPU。

从那以后我每次拿到一个新的识别项目,都强制走一遍「单图推理 → 批量测试 → 边界 case 检查」的流程,不跳过任何一步。这份源码的价值不在于它多完美,而在于它给了一个能跑通的起点,你在这个起点上改网络、换数据、调参数,都比从零搭要快得多。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 1:28:54

康华光模电第6版电子书下载与使用全指南:从筛选到实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:28:27

LTspice波形源生成指南:正弦波、方波、三角波与锯齿波参数详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:28:26

STM32固件bin完整提取指南:从STLink连接到Option Bytes验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:27:47

OG05B1B全局快门Sensor驱动开发实战:从上电时序到多Sensor同步

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:27:41

嵌入式开发三步闭环:编译、烧录与仿真的底层原理与协同调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:27:27

新能源汽车销量预测:ARIMA参数落地与Python复现实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华