简介这是一套面向计算机相关专业学生与深度学习入门者的车牌识别实战项目采用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输入分辨率车牌占画面比例小时可提到 960conf0.25推理置信度阈值夜间误检多时提到 0.4iou0.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.103.11 以上有些 torch 版本还没跟上。用 conda 建环境比裸 pip 稳因为 CUDA 版本和 torch 的对应关系容易搞混。conda create -n lpr python3.10 -y conda activate lpr pip install torch2.1.0 torchvision0.16.0 --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics opencv-python numpy pillowtorch的 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( sourcetest_images/car_night.jpg, imgsz640, conf0.25, iou0.5, saveTrue, # 保存带框结果图 save_txtTrue, # 保存框坐标 txt projectruns/detect, nameplate_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(fcrops/plate_{x1}_{y1}.jpg, plate_crop)saveTrue会把画框后的图存到runs/detect/plate_test/save_txtTrue存归一化坐标。裁出来的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_numlen(CHARS)) net.load_state_dict(torch.load(weights/lprnet_best.pth, map_locationcpu)) 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(dim2).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(f0 {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.yamlpath: ./datasets/plate train: images/train val: images/val names: 0: plate训练命令yolo detect train modelyolo11s.pt dataplate.yaml epochs100 imgsz640 batch16 patience20训练过程中看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(clipLimit2.0, tileGridSize(8,8))在灰度通道上做一次再送进检测模型召回能回来一部分。5.5 现象训练 loss 震荡不收敛原因可能是学习率太大、batch 太小、或者标注里有空文件。解决先把学习率降到 0.001 试 10 个 epoch如果还震荡检查labels目录里有没有 0 字节的 txt空标注会让 loss 计算异常。另外mosaic增强在训练后期可以关掉用close_mosaic10让最后 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_numlen(CHARS)) rec_model.load_state_dict(torch.load(weights/lprnet_best.pth, map_locationcpu)) 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(fval_images/{fname}) results det_model.predict(img, imgsz640, conf0.25, verboseFalse) 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(dim2).squeeze().cpu().numpy() pred_str decode(pred) if pred_str gt: correct 1 total 1 print(f端到端准确率: {correct / total:.4f})这段脚本的关键在于文件名即真值所以验证集图片命名要规范。如果准确率低于 0.85先分开看是检测漏框还是识别错字。检测漏框就调conf识别错字就补对应字符的样本。我一般会把这个脚本跑三遍分别用conf0.25、0.35、0.45看哪个阈值下端到端最高再定最终部署参数。另外如果部署到 Jetson Nano记得把模型转成 TensorRTYOLOv11 用yolo export formatengineLPRNet 用torch.onnx.export再转推理速度能翻倍。这套流程我踩过最大的坑是验证集文件名里有空格或特殊字符导致真值比对失败准确率直接掉到 0.3查了一下午才发现是命名问题。希望帮到你。本文还有配套的精品资源点击获取