拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于YOLOv11与LPRNet的车牌识别系统:从检测到端到端部署

基于YOLOv11与LPRNet的车牌识别系统:从检测到端到端部署

简介:这是一套面向计算机相关专业学生与深度学习入门者的车牌识别实战项目,采用YOLOv11完成车牌检测、LPRNet实现字符识别,可作为毕业设计、课程设计或期末大作业的完整参考方案。资源包共209个文件,约148.26MB,包含23个Python源码、22个Jupyter Notebook实验记录、16个pth与4个pt模型权重、1个onnx导出模型,以及90张png与27张jpg图像、yaml配置、ttc/ttf字体和csv训练日志,覆盖数据准备、模型训练、推理部署全流程。已有185人学习关注,说明该方案具备一定参考价值。读者可获得可直接运行的完整代码、预训练模型与数据集,借助Notebook逐步复现训练与测试过程,理解检测与识别两阶段的衔接逻辑,并在此基础上替换数据集或调整网络结构,快速完成自己的课题任务。

1. 从一张糊到看不清的夜间车牌说起:YOLOv11 + LPRNet 到底解决什么问题

地下车库出口,逆光、车牌倾斜、泥点糊住半个字,传统 OpenCV 那套「灰度 → 边缘 → 形态学 → 字符切分」的流水线当场翻车。这不是调参能救的,是整条技术路线在复杂光照下就站不住。基于 YOLOv11 和 LPRNet 的车牌识别系统,思路是把「找车牌」和「读车牌」拆成两个独立模型:YOLOv11 负责在整张图里框出车牌位置,LPRNet 负责把裁出来的车牌小图直接映射成字符串,中间不做字符切分。这套组合在中文车牌上能做到端到端可训练、可替换、可部署,也是目前计算机毕业设计里性价比最高的一条路线——数据集公开、模型文件能直接跑、Python 源码改起来门槛低。如果你正在做计算机毕业设计,或者想找一个能塞进 Jetson Nano 的完整视觉项目练手,这篇就是按「先跑通、再换数据、最后调优」的顺序把整条链路讲清楚。

2. 拆开看:YOLOv11 检测头与 LPRNet 序列识别的分工逻辑

2.1 为什么不用一个模型端到端搞定

很多人第一反应是「检测+识别用一个网络不就行了」。理论上可以,但工程上不划算。车牌检测需要的是位置回归,识别需要的是序列建模,两个任务的损失函数、数据增强策略、输入分辨率都不一样。YOLOv11 在 640×640 输入下对小目标已经做了结构优化,C3k2 模块和 SPPF 的组合让它在车牌这种细长目标上召回率比前代高;LPRNet 则是纯卷积结构,没有 RNN,靠 CTC 损失直接输出字符序列,推理速度快到可以在 CPU 上跑。分开训练的好处是:检测模型换数据只影响框,识别模型换省份简称只影响字符集,互不牵连。常见做法是先把 YOLOv11 训到 mAP50 稳定在 0.95 以上,再冻结检测结果去训 LPRNet,这样识别模型的输入分布是固定的,收敛更快。

2.2 YOLOv11 检测部分的三个必调参数

YOLOv11 的配置文件里,和车牌检测最相关的是这三个:

参数推荐值作用
imgsz640输入分辨率,车牌占画面比例小时可提到 960
conf0.25推理置信度阈值,夜间误检多时提到 0.4
iou0.5NMS 重叠阈值,车牌密集场景降到 0.4

训练时batch根据显存来,8G 显存跑yolov11s用 16,跑yolov11m用 8。epochs建议 100 起步,但要看早停,patience 设 20。数据增强里mosaic对车牌有帮助,但mixup要关掉,因为混合两张图会让车牌字符重叠,识别模型学不到干净样本。

2.3 LPRNet 的字符集与 CTC 解码

LPRNet 的输出不是固定长度,而是用 CTC 把卷积特征序列映射到字符序列。中文车牌字符集一般是 68 个左右:省份简称 31 个、字母 24 个、数字 10 个、特殊字符若干。定义字符集时顺序必须固定,训练和推理用同一份映射表。CTC 解码用贪心策略就够,beam search 在车牌场景提升有限但耗时翻倍。LPRNet 输入尺寸通常归一化到 94×24,这个尺寸是经过验证的,再大对精度提升不明显,再小会丢笔画。

3. 把环境跑起来:从 Python 安装到 YOLOv11 推理出第一张结果图

3.1 环境配置的版本锁定

Python 版本建议 3.9 或 3.10,3.11 以上有些 torch 版本还没跟上。用 conda 建环境比裸 pip 稳,因为 CUDA 版本和 torch 的对应关系容易搞混。

conda create -n lpr python=3.10 -y conda activate lpr pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics opencv-python numpy pillow

torch的 CUDA 版本要和本机驱动匹配,nvidia-smi右上角显示的 CUDA Version 是驱动支持的上限,不是必须装那个版本。装完用python -c "import torch; print(torch.cuda.is_available())"验证,返回 True 才算通。如果 False,先检查驱动,再检查是不是装了 CPU 版 torch。

3.2 用 YOLOv11 跑通检测并保存推理结果

模型文件拿到后,先别急着训练,用官方权重或项目自带权重跑一张图,确认推理链路是通的。

from ultralytics import YOLO import cv2 model = YOLO("weights/yolo11_best.pt") # 项目自带的车牌检测权重 results = model.predict( source="test_images/car_night.jpg", imgsz=640, conf=0.25, iou=0.5, save=True, # 保存带框结果图 save_txt=True, # 保存框坐标 txt project="runs/detect", name="plate_test" ) for r in results: boxes = r.boxes.xyxy.cpu().numpy() print(f"检测到 {len(boxes)} 个车牌") for b in boxes: x1, y1, x2, y2 = map(int, b[:4]) plate_crop = r.orig_img[y1:y2, x1:x2] cv2.imwrite(f"crops/plate_{x1}_{y1}.jpg", plate_crop)

save=True会把画框后的图存到runs/detect/plate_test/,save_txt=True存归一化坐标。裁出来的plate_crop就是下一步 LPRNet 的输入。注意r.orig_img是原始 BGR 图,不是缩放后的,所以坐标直接能用。如果检测框偏大或偏小,先看conf是不是太低导致误检,再看训练数据里车牌的标注框是不是贴太紧。

3.3 LPRNet 推理与结果拼接

LPRNet 的推理代码通常项目里会带一个inference.py或demo.py,核心逻辑是加载权重、预处理、前向、CTC 解码。

import torch import cv2 import numpy as np from model.lprnet import LPRNet, CHARS net = LPRNet(class_num=len(CHARS)) net.load_state_dict(torch.load("weights/lprnet_best.pth", map_location="cpu")) net.eval() def preprocess(img): img = cv2.resize(img, (94, 24)) img = img.astype(np.float32) img = (img - 127.5) * 0.0078125 # 归一化到 [-1,1] img = img.transpose(2, 0, 1) # HWC -> CHW return torch.from_numpy(img).unsqueeze(0) crop = cv2.imread("crops/plate_100_200.jpg") with torch.no_grad(): pred = net(preprocess(crop)) pred = pred.argmax(dim=2).squeeze().cpu().numpy() # CTC 贪心解码:去重复、去 blank result = [] prev = -1 for p in pred: if p != prev and p != len(CHARS) - 1: result.append(CHARS[p]) prev = p print("识别结果:", "".join(result))

预处理里的127.5和0.0078125是 LPRNet 原论文的归一化参数,换成别的值精度会掉。CTC 解码时 blank 的索引是字符集最后一个,去重逻辑是「当前字符不等于前一个且不是 blank」。如果识别结果出现重复字符,检查去重逻辑是不是写成了只去 blank 没去连续重复。

4. 换自己的数据集:标注、转换、训练与 mAP 验证

4.1 车牌数据标注的四个边界坑

用 LabelImg 或 Roboflow 标车牌,类别名统一写plate,不要写license_plate或chepai,因为后面训练脚本里的names要对上。标注框要贴紧车牌边缘,但不要把车牌外的螺丝孔或边框包进去,否则 LPRNet 裁出来的图会多一圈干扰。倾斜车牌用旋转框标当然更好,但 YOLOv11 默认是水平框,所以尽量选倾斜角度小于 15 度的样本,超过的要么剔除要么做透视矫正。夜间样本的框要尤其仔细,因为暗部边缘肉眼难辨,标松了检测模型学不准。

4.2 VOC 转 YOLO 格式的脚本与参数

如果拿到的数据集是 VOC 的 XML,需要转成 YOLO 的 txt。

import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, img_w, img_h, out_path): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter("object"): cls_name = obj.find("name").text if cls_name != "plate": continue bnd = obj.find("bndbox") x1 = float(bnd.find("xmin").text) y1 = float(bnd.find("ymin").text) x2 = float(bnd.find("xmax").text) y2 = float(bnd.find("ymax").text) cx = (x1 + x2) / 2 / img_w cy = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"0 {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(out_path, "w") as f: f.write("\n".join(lines))

img_w和img_h必须从对应图片读,不能写死。归一化坐标保留 6 位小数,少了会有精度损失。转换完抽查几张,用cv2.rectangle把框画回去看是否对齐,这一步省了后面训练 loss 不降会查很久。

4.3 YOLOv11 训练命令与 mAP 看板

数据按images/train、images/val、labels/train、labels/val放好,写一个plate.yaml:

path: ./datasets/plate train: images/train val: images/val names: 0: plate

训练命令:

yolo detect train model=yolo11s.pt data=plate.yaml epochs=100 imgsz=640 batch=16 patience=20

训练过程中看runs/detect/train/results.csv,重点看metrics/mAP50和metrics/mAP50-95。车牌检测 mAP50 到 0.95 以上算合格,如果卡在 0.8 上不去,先看验证集里有没有训练集没出现过的场景(比如夜间、雨天),再检查标注框是不是有大量偏移。yolo11s在 8G 显存上跑 640 输入 batch 16 大概占 6G,如果 OOM 就降到 8。

4.4 LPRNet 训练的数据对齐

LPRNet 训练需要「车牌小图 + 对应字符串」的配对。可以从检测数据集的标注框裁图,文件名用字符串命名,比如京A12345.jpg。训练时读文件名当标签,按字符集转成索引序列。CTC 损失要求输入序列长度大于标签长度,LPRNet 输出序列长度是 24 左右,车牌 7 个字符,满足条件。如果训练 loss 不降,先检查字符集映射有没有漏字符,再检查裁图是不是把车牌裁歪了。

5. 避坑与排查:从环境到推理的五个血泪现场

5.1 现象:torch.cuda.is_available() 返回 False

原因通常是装了 CPU 版 torch,或者 CUDA 版本和驱动不匹配。解决:先nvidia-smi看驱动支持的 CUDA 上限,再去 pytorch 官网找对应版本的安装命令,不要直接pip install torch。如果驱动太旧,升级驱动比降 torch 版本更省事。

5.2 现象:YOLOv11 推理结果框全图或框不住车牌

原因多半是conf阈值设太低,或者模型权重和类别数不匹配。解决:先把conf提到 0.4 看框是否收敛到车牌区域,如果还是乱框,检查yolo11_best.pt是不是用当前plate.yaml训出来的,类别数对不上会输出错误框。

5.3 现象:LPRNet 识别结果全是同一个字符

原因是 CTC 解码时 blank 索引搞错了,或者字符集顺序和训练时不一致。解决:打印CHARS列表,确认 blank 在最后一位;再检查训练时的字符集文件和推理时是不是同一个。这个坑很隐蔽,因为模型输出看起来「有结果」,但全是错的。

5.4 现象:夜间车牌检测召回率骤降

原因是训练集里夜间样本太少,模型没学到暗光下的边缘特征。解决:要么补夜间数据,要么在推理前做一次 CLAHE 增强。补数据是根治,增强是应急。如果做毕业设计时间紧,先用cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))在灰度通道上做一次,再送进检测模型,召回能回来一部分。

5.5 现象:训练 loss 震荡不收敛

原因可能是学习率太大、batch 太小、或者标注里有空文件。解决:先把学习率降到 0.001 试 10 个 epoch,如果还震荡,检查labels目录里有没有 0 字节的 txt,空标注会让 loss 计算异常。另外mosaic增强在训练后期可以关掉,用close_mosaic=10让最后 10 个 epoch 用原始图微调。

6. 进阶技巧:把检测和识别串成一条流水线并验证端到端准确率

单看检测 mAP 和识别准确率都不够,毕业设计答辩时老师一定会问「端到端准确率多少」。端到端准确率的定义是:检测框 IoU 大于 0.5 且识别字符串完全正确的样本占比。验证方法是从验证集里抽 200 张图,跑完整流水线,逐张比对。

import os import cv2 import numpy as np from ultralytics import YOLO from model.lprnet import LPRNet, CHARS import torch det_model = YOLO("weights/yolo11_best.pt") rec_model = LPRNet(class_num=len(CHARS)) rec_model.load_state_dict(torch.load("weights/lprnet_best.pth", map_location="cpu")) rec_model.eval() def decode(pred): result, prev = [], -1 for p in pred: if p != prev and p != len(CHARS) - 1: result.append(CHARS[p]) prev = p return "".join(result) correct, total = 0, 0 for fname in os.listdir("val_images"): gt = os.path.splitext(fname)[0] # 文件名即真值 img = cv2.imread(f"val_images/{fname}") results = det_model.predict(img, imgsz=640, conf=0.25, verbose=False) if len(results[0].boxes) == 0: total += 1 continue box = results[0].boxes.xyxy[0].cpu().numpy().astype(int) x1, y1, x2, y2 = box crop = img[y1:y2, x1:x2] if crop.size == 0: total += 1 continue inp = cv2.resize(crop, (94, 24)).astype(np.float32) inp = (inp - 127.5) * 0.0078125 inp = torch.from_numpy(inp.transpose(2, 0, 1)).unsqueeze(0) with torch.no_grad(): pred = rec_model(inp).argmax(dim=2).squeeze().cpu().numpy() pred_str = decode(pred) if pred_str == gt: correct += 1 total += 1 print(f"端到端准确率: {correct / total:.4f}")

这段脚本的关键在于文件名即真值,所以验证集图片命名要规范。如果准确率低于 0.85,先分开看是检测漏框还是识别错字。检测漏框就调conf,识别错字就补对应字符的样本。我一般会把这个脚本跑三遍,分别用conf=0.25、0.35、0.45,看哪个阈值下端到端最高,再定最终部署参数。另外,如果部署到 Jetson Nano,记得把模型转成 TensorRT,YOLOv11 用yolo export format=engine,LPRNet 用torch.onnx.export再转,推理速度能翻倍。这套流程我踩过最大的坑是验证集文件名里有空格或特殊字符,导致真值比对失败,准确率直接掉到 0.3,查了一下午才发现是命名问题。希望帮到你。

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

返回列表