尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于YOLOv8和LPRNet的车牌识别系统源码解析与实战调优

基于YOLOv8和LPRNet的车牌识别系统源码解析与实战调优 简介本资源是一套基于 YOLOv8 与 LPRNet 的车牌识别系统完整项目面向计算机、人工智能等专业的学生及开发者可直接用于毕业设计、期末大作业或课程设计。项目将目标检测与字符识别两阶段串联涵盖数据标注、模型训练、推理部署及前端展示等环节代码注释详尽新手也能快速理解整体流程。压缩包共 60 个文件约 31.36MB包含 13 个 Python 源码文件、3 个 pt 模型权重、1 个 pth 权重、3 个 Vue 前端文件以及 json、yaml、Dockerfile、md 等配置与说明文档另附 jpg 示例图片覆盖训练、推理与界面展示所需素材。目前已有 232 人学习下载。项目经过严格调试部署后即可运行读者可据此掌握 YOLOv8 检测与 LPRNet 识别的联合调优思路、数据集制作方法及 Flask 后端与 Vue 前端的对接方式具备较高的实际应用与参考价值。1. 从一张糊到看不清的图说起这套车牌识别源码到底能干什么去年帮一个做停车场系统的朋友看现场出口相机拍回来的图车牌占画面不到八分之一晚上还有车灯眩光传统 OpenCV 那套边缘检测加颜色分割直接崩了识别率掉到六成以下。后来换成检测加识别的两阶段方案先定位车牌再单独识别字符同样的图识别率拉回到九成五以上。这套「基于 YOLOv8 和 LPRNet 的车牌识别系统 python 源码 训练好的模型」走的就是这条路子而且把训练好的权重一起打包了拿到手不用从零训直接能跑推理。它解决的核心问题就一个在复杂光照、倾斜、远距离、多车牌并存的场景下把车牌从整张图里抠出来再把上面的字符逐个认对。适合谁做计算机毕业设计的学生需要一套结构清晰、能讲清楚原理、还能现场演示的系统也适合做停车场、门禁、交通卡口这类项目的工程师想快速搭个能用的基线再往上叠业务逻辑。源码是 Python 的YOLOv8 负责检测LPRNet 负责字符识别两个模型都是现成的省掉了最耗时的标注和训练环节。我拿到这类资源包第一件事不是急着跑而是先看目录结构和依赖确认它到底是「能复现的工程」还是「跑不通的 demo」。这套东西的价值在于它把检测和识别两个环节的接口对齐了中间的数据流转是通的不是两个孤立脚本拼在一起。下面按我实际拆包的顺序把怎么用、参数怎么调、坑在哪一条条讲清楚。2. 拆开资源包先看什么YOLOv8 检测与 LPRNet 识别的接口对齐2.1 两阶段架构为什么比端到端更适合车牌场景车牌识别本质上是个「先定位、后识别」的问题。端到端方案比如直接用一个大模型输出车牌字符串听起来优雅但实际落地时有两个硬伤一是车牌在整图里占比小端到端模型容易被背景干扰二是字符识别对分辨率敏感整图缩放到统一尺寸后车牌区域的信息损失严重。两阶段方案把这两个问题拆开解决——YOLOv8 在原始分辨率上做检测输出车牌框坐标再把框内的区域裁剪出来缩放到 LPRNet 需要的尺寸单独识别。这样检测阶段不受字符分辨率影响识别阶段不受背景干扰。YOLOv8 在这套系统里用的是 nano 或 small 级别的骨干常见做法是选 yolov8n因为车牌检测的目标类别单一就「车牌」一类不需要大模型的特征容量。LPRNet 则是个轻量级卷积网络专门为车牌字符识别设计输入尺寸通常是 94x24 或 120x32输出是字符序列。两个模型的衔接点在于YOLOv8 输出的框要按一定比例外扩常见 5% 到 10%避免车牌边缘字符被裁掉然后再送进 LPRNet。提示外扩比例不是拍脑袋定的跟你的检测框精度有关。如果 YOLOv8 训练时标注框贴得紧外扩 5% 就够如果标注有偏差外扩 10% 更稳但要注意别把相邻车牌框进来。2.2 目录结构与依赖检查先确认能跑再谈调优拿到源码包先别急着pip install按这个顺序过一遍# 第一步看目录结构确认模型文件和数据样例在不在 tree -L 2 ./plate_recognition # 典型输出应该是这样 # ├── models/ # │ ├── yolov8n_plate.pt # YOLOv8 检测权重 # │ └── lprnet_final.pth # LPRNet 识别权重 # ├── utils/ # │ ├── detector.py # 检测封装 # │ ├── recognizer.py # 识别封装 # │ └── plate_utils.py # 坐标变换、字符映射 # ├── configs/ # │ └── config.yaml # 阈值、尺寸、字符集配置 # ├── demo.py # 单图/批量推理入口 # └── requirements.txt# 第二步检查依赖版本重点看 torch 和 ultralytics cat requirements.txt # 常见内容 # torch1.8.0 # ultralytics8.0.0 # opencv-python4.5.0 # numpy # pillow# 第三步确认权重文件完整性别下到一半的残包 ls -lh models/ # yolov8n_plate.pt 一般在 6MB 左右lprnet_final.pth 在 2MB 上下 # 如果明显偏小说明文件没下全重新获取逻辑说明第一步的目录结构决定了你后面改代码时知道去哪找。第二步的依赖里ultralytics是 YOLOv8 的官方库版本低于 8.0 会缺 APItorch版本跟你的 CUDA 有关CPU 跑也能用但慢。第三步是血泪经验我见过好几次权重文件只有几百 KB跑起来报unexpected EOF查半天以为是代码问题其实是下载断了。参数说明config.yaml里通常有几个关键项——det_conf_thres检测置信度阈值默认 0.25、det_iou_thresNMS 的 IoU 阈值默认 0.45、input_sizeLPRNet 输入尺寸、chars字符集包含省份简称和字母数字。这些值先别改跑通默认配置再说。2.3 跑通第一张图从命令行到可视化输出# demo.py 的核心推理流程简化版方便理解数据流转 import cv2 import torch from ultralytics import YOLO from utils.recognizer import LPRNet from utils.plate_utils import decode_plate, expand_box # 加载两个模型 detector YOLO(models/yolov8n_plate.pt) recognizer LPRNet(charsconfig[chars]) recognizer.load_state_dict(torch.load(models/lprnet_final.pth)) recognizer.eval() # 读图 img cv2.imread(test.jpg) # 检测YOLOv8 返回框和置信度 results detector(img, conf0.25, iou0.45) boxes results[0].boxes.xyxy.cpu().numpy() for box in boxes: # 外扩 8%防止边缘字符被裁 x1, y1, x2, y2 expand_box(box, ratio0.08, img_shapeimg.shape) plate_crop img[y1:y2, x1:x2] # 缩放到 LPRNet 输入尺寸 plate_resized cv2.resize(plate_crop, (94, 24)) # 识别 plate_text recognizer.predict(plate_resized) # 画框和文字 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)逻辑说明这段代码把两个模型串起来了。detector(img)返回的是 YOLOv8 的标准输出boxes.xyxy是左上右下坐标。expand_box做外扩cv2.resize把裁剪区域统一到 LPRNet 的输入尺寸。recognizer.predict内部会做归一化和 CTC 解码输出字符串。参数说明conf0.25是检测阈值调高会漏检、调低会误检iou0.45控制 NMS 合并重叠框的力度ratio0.08是外扩比例前面说过跟标注精度有关(94, 24)是 LPRNet 的输入尺寸必须跟训练时一致改了会识别乱码。跑通这张图之后你会得到一张画了框和识别结果的图。如果框位置对但文字错问题在识别模型或字符集如果框都没出来问题在检测阈值或权重加载。这一步是分水岭过了才能往下调。3. 把模型跑出稳定结果阈值、尺寸与批处理的参数怎么设3.1 检测阈值与 NMS 的联动调法YOLOv8 的conf和iou两个参数不是独立的调的时候要一起看。conf决定哪些框进入候选iou决定候选框怎么合并。常见误区是只调conf不调iou结果要么一堆重叠框要么相邻车牌被合并成一个。我一般按这个顺序调先把iou固定在 0.45conf从 0.5 往下试到 0.15看漏检和误检的平衡点。车牌场景下如果图里车牌清晰conf0.4就够如果远距离小目标多降到 0.2 甚至 0.15。然后固定confiou从 0.3 试到 0.6如果发现两个车牌挨得近被合并把iou降到 0.3 到 0.35如果同一个车牌出多个框把iou升到 0.5 以上。# 批量测试不同阈值组合找最优 import itertools import cv2 from ultralytics import YOLO detector YOLO(models/yolov8n_plate.pt) img cv2.imread(test_multi.jpg) for conf, iou in itertools.product([0.15, 0.25, 0.35, 0.45], [0.3, 0.4, 0.5]): results detector(img, confconf, iouiou, verboseFalse) n_boxes len(results[0].boxes) print(fconf{conf}, iou{iou}, boxes{n_boxes})逻辑说明这段脚本遍历阈值组合打印每种组合检出的框数。框数突然跳变的地方就是阈值敏感区选跳变前的稳定值。参数说明verboseFalse关掉 YOLO 的日志输出批量测试时清爽。实际部署时conf和iou应该写进配置文件不要硬编码在代码里方便现场调。3.2 LPRNet 输入尺寸与字符集的对应关系LPRNet 的输入尺寸和字符集是绑定的。常见的中文车牌字符集是 68 个字符省份简称 31 个 字母 24 个 数字 10 个 特殊字符加上 CTC 的 blank 标签输出维度是 69。如果你拿到的权重是在 94x24 上训的推理时改成 120x32识别结果会全乱因为卷积层的感受野和位置编码对不上。# 检查字符集和输入尺寸是否匹配 import yaml with open(configs/config.yaml, r, encodingutf-8) as f: config yaml.safe_load(f) print(f输入尺寸: {config[input_size]}) # 应该是 [94, 24] 或 [120, 32] print(f字符集长度: {len(config[chars])}) # 中文车牌一般是 68 print(f字符集前几个: {config[chars][:5]}) # 看是不是省份简称开头逻辑说明这段代码读配置确认输入尺寸和字符集。如果字符集长度对不上权重文件的输出维度加载权重时会报size mismatch。参数说明input_size是列表[width, height]注意顺序OpenCV 的 resize 是(width, height)别搞反。chars是字符串或列表顺序必须跟训练时一致否则解码出来的字符会错位。3.3 批处理与 GPU 显存什么时候该分批如果一次要处理几百张图逐张推理太慢。YOLOv8 支持批处理LPRNet 也可以。但批处理大小受显存限制常见做法是检测阶段用 batch8 或 16识别阶段因为裁剪区域尺寸小可以 batch32 甚至 64。# 批处理推理示例 import glob import cv2 import torch from ultralytics import YOLO detector YOLO(models/yolov8n_plate.pt) img_paths glob.glob(test_imgs/*.jpg) # 检测阶段批处理 results detector(img_paths, conf0.25, iou0.45, batch8) # 收集所有裁剪区域 all_crops [] for i, r in enumerate(results): img cv2.imread(img_paths[i]) for box in r.boxes.xyxy.cpu().numpy(): x1, y1, x2, y2 expand_box(box, 0.08, img.shape) crop cv2.resize(img[y1:y2, x1:x2], (94, 24)) all_crops.append(crop) # 识别阶段批处理 crops_tensor torch.stack([torch.from_numpy(c).permute(2,0,1).float()/255 for c in all_crops]) with torch.no_grad(): for i in range(0, len(crops_tensor), 32): batch crops_tensor[i:i32] texts recognizer.predict_batch(batch)逻辑说明检测阶段把图片路径列表传给 YOLO它内部会分批。识别阶段先把所有裁剪区域收集起来再按 32 一批送进 LPRNet。这样比逐张快三到五倍。参数说明batch8是检测的批大小显存不够就降到 4 或 2。识别阶段的 32 可以根据显存调CPU 跑的话建议降到 8。torch.no_grad()关掉梯度计算省显存。注意批处理时图片尺寸不一致会导致 YOLO 内部做 paddingpadding 太多会影响检测精度。常见做法是先把图片缩放到相近尺寸再批处理或者干脆逐张检测、批量识别。4. 避坑与排查这套源码跑不起来时先查这五处4.1 现象报错No module named ultralytics或版本不兼容原因ultralytics没装或者装了旧版7.xAPI 跟 YOLOv8 不兼容。YOLOv8 的YOLO类和model.predict接口在 8.0 之后才稳定。解决先卸载旧版再装新版。pip uninstall ultralytics -y pip install ultralytics8.0.0 # 如果还报错检查 torch 版本 python -c import torch; print(torch.__version__) # torch 低于 1.8 的话ultralytics 8.x 跑不了升级 torch4.2 现象权重加载报unexpected key或size mismatch原因权重文件跟模型定义对不上。常见情况是 LPRNet 的字符集改了输出层维度变了但权重还是旧的或者 YOLOv8 权重是分割模型-seg而不是检测模型。解决先确认权重类型和字符集。# 检查 YOLOv8 权重类型 from ultralytics import YOLO model YOLO(models/yolov8n_plate.pt) print(model.task) # 应该是 detect如果是 segment 就错了# 检查 LPRNet 权重输出维度 import torch state_dict torch.load(models/lprnet_final.pth, map_locationcpu) # 找最后一层全连接或卷积的输出维度 for k, v in state_dict.items(): if output in k or fc in k: print(k, v.shape)逻辑说明model.task告诉你权重是检测还是分割。LPRNet 的输出维度要跟字符集长度加一CTC blank对上。4.3 现象识别结果全是重复字符或空白原因CTC 解码出问题。常见是输入图片没做归一化或者解码时没去重、没去 blank。解决检查预处理和解码逻辑。# 正确的预处理归一化到 [0, 1] 并转 tensor crop cv2.resize(plate_crop, (94, 24)) crop crop.astype(float32) / 255.0 crop torch.from_numpy(crop).permute(2, 0, 1).unsqueeze(0) # 正确的 CTC 解码去重、去 blank def ctc_decode(logits, chars, blank0): indices torch.argmax(logits, dim2).squeeze().cpu().numpy() result [] prev -1 for idx in indices: if idx ! blank and idx ! prev: result.append(chars[idx - 1]) # 减 1 是因为 blank 占了 0 prev idx return .join(result)逻辑说明归一化是必须的LPRNet 训练时输入就是 [0,1]。CTC 解码的核心是「合并重复、去掉 blank」漏了任何一步都会输出乱码。4.4 现象CPU 上跑得极慢一张图要好几秒原因默认用了 CPU 推理或者 torch 没装对应 CUDA 版本。YOLOv8 在 CPU 上跑 nano 模型大概 200 到 500 毫秒一张LPRNet 更慢。解决确认 GPU 可用并指定设备。import torch print(torch.cuda.is_available()) # True 才能用 GPU # 推理时指定 device results detector(img, devicecuda:0 if torch.cuda.is_available() else cpu) recognizer recognizer.to(cuda if torch.cuda.is_available() else cpu)参数说明devicecuda:0指定第一块 GPU。如果有多块可以选cuda:1。CPU 跑的话把 YOLO 的imgsz降到 320 能提速但小目标检测会变差。4.5 现象中文车牌识别出字母数字省份简称丢了原因字符集里没有中文省份简称或者训练数据里中文样本太少。LPRNet 的字符集如果只包含字母数字中文自然识别不出来。解决检查字符集是否包含省份简称。# 常见的中文车牌字符集应该包含这些 provinces 京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼 letters ABCDEFGHIJKLMNOPQRSTUVWXYZ digits 0123456789 full_chars provinces letters digits print(f完整字符集长度: {len(full_chars)}) # 应该是 68逻辑说明如果配置里的chars长度明显小于 68说明省份简称缺失。这种情况要么换权重要么自己补训练数据重新训 LPRNet。5. 从能跑到好用置信度过滤、多帧投票与结果后处理5.1 用置信度做二次过滤压掉误检YOLOv8 输出的每个框都带置信度但默认的conf阈值是全局的。实际场景里有些误检框的置信度在 0.3 到 0.4 之间跟真车牌混在一起。我一般会在检测之后再加一层过滤对每个框把裁剪区域送进 LPRNet如果识别结果的字符数不在合理范围比如中文车牌 7 到 8 位或者识别置信度低于某个值就丢掉这个框。# 二次过滤结合检测置信度和识别置信度 def filter_plates(results, recognizer, img, det_thres0.3, rec_thres0.8): valid_plates [] for box in results[0].boxes: det_conf box.conf.item() if det_conf det_thres: continue x1, y1, x2, y2 box.xyxy.cpu().numpy()[0] crop cv2.resize(img[int(y1):int(y2), int(x1):int(x2)], (94, 24)) text, rec_conf recognizer.predict_with_conf(crop) # 字符数合理且识别置信度够高 if 6 len(text) 9 and rec_conf rec_thres: valid_plates.append((text, det_conf * rec_conf)) return valid_plates逻辑说明det_conf * rec_conf作为综合得分比单看检测置信度更稳。字符数范围 6 到 9 覆盖了普通车牌和新能源车牌。参数说明det_thres0.3比默认的 0.25 稍高压误检rec_thres0.8是识别置信度门槛调高会漏掉模糊车牌调低会引入错误结果。这两个值要根据你的场景试。5.2 多帧投票视频流里把抖动压下去如果是视频流同一辆车连续多帧都会被检测到。单帧识别可能因为运动模糊出错但多帧投票能纠正。常见做法是维护一个滑动窗口对同一个车牌位置用 IoU 匹配的识别结果做多数投票。from collections import deque, Counter class PlateVoter: def __init__(self, window5): self.window window self.history deque(maxlenwindow) def add(self, plate_text): self.history.append(plate_text) def get_stable(self): if len(self.history) 3: return None # 多数投票 counter Counter(self.history) text, count counter.most_common(1)[0] # 至少 60% 的帧认同才输出 if count / len(self.history) 0.6: return text return None逻辑说明deque(maxlen5)维护最近 5 帧的结果Counter统计出现次数超过 60% 才认为稳定。这样单帧的偶发错误会被压掉。参数说明window5是窗口大小视频帧率高可以调到 7 或 9帧率低就降到 3。0.6是投票阈值调高更稳但响应慢调低响应快但可能输出错误结果。5.3 结果后处理省份简称纠错与格式校验LPRNet 的输出偶尔会把相似的字符认错比如「京」和「津」、「0」和「O」。常见做法是加一层规则校验第一位必须是省份简称第二位必须是字母后面是字母数字混合。如果不符合尝试用编辑距离找最接近的合法车牌。# 简单的格式校验和纠错 PROVINCES set(京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼) LETTERS set(ABCDEFGHIJKLMNOPQRSTUVWXYZ) DIGITS set(0123456789) def validate_plate(text): if len(text) 7: return None # 第一位必须是省份简称 if text[0] not in PROVINCES: return None # 第二位必须是字母 if text[1] not in LETTERS: return None # 后面是字母数字 for ch in text[2:]: if ch not in LETTERS and ch not in DIGITS: return None return text逻辑说明这个校验函数把明显不合法的结果直接过滤掉。如果识别出「京A12345」通过如果识别出「京A12O45」字母 O 混进数字位可以加一步把 O 替换成 0 再校验。参数说明PROVINCES集合可以根据你的实际场景增减比如只做某个省的业务就只保留那个省的简称。校验规则越严误识率越低但漏识率会上升要平衡。5.4 一个完整的推理脚本骨架把前面的东西串起来一个能直接用的推理脚本大概长这样import cv2 import torch import yaml from ultralytics import YOLO from utils.recognizer import LPRNet from utils.plate_utils import expand_box, ctc_decode # 加载配置 with open(configs/config.yaml, r, encodingutf-8) as f: config yaml.safe_load(f) # 初始化模型 device cuda if torch.cuda.is_available() else cpu detector YOLO(models/yolov8n_plate.pt).to(device) recognizer LPRNet(charsconfig[chars]).to(device) recognizer.load_state_dict(torch.load(models/lprnet_final.pth, map_locationdevice)) recognizer.eval() def recognize_image(img_path): img cv2.imread(img_path) results detector(img, conf0.25, iou0.45, devicedevice) plates [] for box in results[0].boxes: x1, y1, x2, y2 expand_box(box.xyxy.cpu().numpy()[0], 0.08, img.shape) crop cv2.resize(img[y1:y2, x1:x2], tuple(config[input_size])) crop crop.astype(float32) / 255.0 crop torch.from_numpy(crop).permute(2,0,1).unsqueeze(0).to(device) with torch.no_grad(): logits recognizer(crop) text ctc_decode(logits, config[chars]) if validate_plate(text): plates.append(text) return plates if __name__ __main__: print(recognize_image(test.jpg))逻辑说明这个骨架把配置加载、模型初始化、推理、后处理都串起来了。expand_box和ctc_decode从 utils 里导入保持代码整洁。参数说明map_locationdevice确保权重加载到正确的设备。tuple(config[input_size])把列表转成元组因为cv2.resize要元组。validate_plate是前面定义的校验函数。从那以后我每次拿到新的识别模型都强制先跑一遍阈值扫描和格式校验确认误检和漏检在可接受范围再往下做业务。这套源码的价值不在于它多完美而在于它把两阶段车牌识别的完整链路摊开了你能看到每个环节的输入输出改起来有抓手。希望帮到你。本文还有配套的精品资源点击获取
返回列表