ARTICLE DETAIL

资讯详情

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

YOLOv8+LPRNet车牌识别实战:检测识别训练部署全攻略

YOLOv8+LPRNet车牌识别实战:检测识别训练部署全攻略 简介这是一份基于YOLOv8目标检测与LPRNet车牌识别相结合的中文车牌识别系统完整工程适合计算机、电子信息等专业课程设计、期末大作业及毕业设计使用。项目采用Python实现涵盖Flask后端服务、Vue前端界面、YOLOv8模型训练与推理代码、LPRNet识别网络以及预训练权重文件形成从车牌检测、字符识别到前后端交互的完整流程。压缩包内共60个文件主要包含py源码、pt/pth模型、jpg样本图片、vue前端组件及json配置等整体36.15MB目录划分清晰便于查找与二次开发。已有1171人浏览学习。资源不仅提供可直接运行的完整源码与训练好的模型还附带数据生成、批量图片处理、标注制作、数据集划分等辅助脚本方便深入理解算法原理并快速改造适配新场景是上手车牌识别项目的优质参考。1. 这套车牌识别系统到底解决什么问题停车场道闸、小区出入口、园区门禁摄像头拍到一辆车系统要在几百毫秒内把车牌号读出来交给后端计费或放行。这个场景里最难的不是“识别字符”而是在一张 1080p 的画面里先找到那块只有几十像素宽的蓝色或绿色牌子再把它上面的字读准。基于 YOLOv8 和 LPRNet 的车牌识别系统就是典型的“先检测、后识别”两阶段方案YOLOv8 负责从整帧图中把车牌区域框出来LPRNet 负责对裁剪后的车牌图做不定长字符识别。这套组合对硬件要求不高训练数据也相对好凑是当前做车牌识别落地最稳的路线之一。适合刚接触车牌识别、手里有 python 基础、想用源码快速跑通并继续调优的开发者。2. 方案拆解为什么 YOLOv8 检测加 LPRNet 识别比端到端更稳2.1 检测和识别拆开模型各干各的才不容易翻车最早不少项目尝试用一个模型直接从原图回归车牌字符序列省去中间环节。但实际跑下来会发现一个尴尬问题车牌在原图里的目标太小一个 1080p 画面里车牌可能只占 100×30 像素让模型同时学习“在哪”和“是什么”两件事收敛慢不说检测漏了直接连识别机会都没有。拆成两阶段之后每个模型只干一件事。YOLOv8 只回答“车牌框在哪”不关心框里是什么字LPRNet 只回答“这张裁剪图里是什么车牌号”不关心它在画面哪个位置。这样做有三个直接好处一是两个模型可以分别换数据集、单独调参数不会牵一发动全身二是排查问题非常方便——识别错了先看检测框准不准框没问题再怀疑识别模型三是中间产物是车牌图可以直接落盘可视化比只看最终结果更容易定位是哪一侧翻车。我自己做这个方向时最先确认的就是两阶段里每个环节的输出长什么样。检测模型输出一个框识别模型输出一串字符中间任何一步出问题都能明确指出来。这条设计原则在车牌这类“小目标 强时序文本”的场景里比端到端省心得多。2.2 LPRNet 为什么不需要逐字符分割车牌识别和一般 OCR 不一样的地方在于字符长度不固定。蓝牌民用车是 7 位新能源绿牌是 8 位警车、使馆车又有各自规则。传统做法先做字符分割把每个字抠出来再逐个分类遇到字符粘连、铆钉遮挡、倾斜视角时分割就崩。LPRNet 的设计绕开了这个瓶颈。LPRNet 是 CNN 加 RNN 再加 CTC 的结构CNN 从车牌图中提取视觉特征RNN 建模字符之间的序列关系CTC 负责把不定长的预测序列对齐到不定长的标签序列。训练时不需要知道每个字符的精确位置只需要给它“这张图对应哪个车牌号”这个整体标注。这个特性和车牌场景天然匹配——车牌字符排列相对规整但允许轻微伸缩和形变CTC 能容忍这种对齐误差。模型的输入常见做法是固定成 94×24 的灰度图输出形状是 (B, T, num_classes)T 是时间步数由输入宽度和网络下采样倍率共同决定num_classes 是字符类别总数加一个 blank。很多人第一次看到输出维度会误以为 T 等于字符个数其实 T 是序列帧数每个帧对应当前位置“最可能是哪个字符”的概率分布。网上流传的 yolov8 网络结构图通常画得很复杂实际用的时候你不用太关心 backbone 里的每个细节只需要知道 YOLOv8 会输出三个尺度的特征图分别负责检测大、中、小目标而车牌在画面里通常算中小目标所以训练时对小尺度特征图的关注度要够。这也是后面我建议关闭 mosaic 增强的原因之一。2.3 yolov8 环境配置与 python 版本选择先把地基打稳源码到手第一步不是看模型而是把环境跑通。常见做法是用 python 3.8 或 3.10检测侧装 ultralytics 包识别侧用 PyTorch 自己搭 LPRNet另外需要 opencv-python、numpy、onnxruntime后续部署用。GPU 显存不够不用慌GTX 1660Ti 这种 6G 显存跑 yolov8n 完全没问题batch 调小一点就行实在没有 GPUCPU 也能训练只是每一轮会慢很多建议先用小数据集验证流程。项目目录我一般这样组织plate_recognition/ ├── detect/ │ ├── datasets/ # 检测数据集 │ ├── weights/ # 检测权重 │ └── runs/ # 训练输出 ├── recog/ │ ├── synth_data/ # LPRNet 合成数据 │ ├── ckpt/ # 识别权重 │ └── train.py └── main.py # 推理串联入口这个结构不是死的但坚持“检测和识别分开目录”能避免后期混淆。环境配置最常见的坑是 torch 的 CUDA 版本和显卡驱动不匹配装完先跑一句python -c import torch; print(torch.cuda.is_available())输出 True 再继续否则后面所有训练都要白等。python 选 3.8 或 3.10 都行ultralytics 对这两者兼容性都很好别用太新的版本去赌第三方库的兼容性。3. 从零训练两个模型YOLOv8 定位车牌LPRNet 识别字符3.1 把公开车牌数据集转成 YOLO 格式解析 CCPD 文件名训练检测模型需要带车牌框的数据。公开数据里 CCPD 是比较常用的车牌数据集它的标注信息直接编码在文件名里拿到图片后要先解析出四个角点再换算成 YOLO 需要的中心点加宽高格式。文件名格式中按-分割后第四段就是车牌四个角的坐标每两个数字一组用_分隔四个点。import cv2 import numpy as np import glob import os def ccpd_parse_to_yolo(img_path, out_dir): img cv2.imread(img_path) h, w img.shape[:2] base os.path.basename(img_path).split(.)[0] parts base.split(-) # 第四段是角点坐标例如: 386473_177454_154383_363402 corners [] for pair in parts[3].split(_): x, y pair.split() corners.append([float(x), float(y)]) corners np.array(corners, dtypenp.float32) # 由四个角点求最小外接矩形得到一个不倾斜的包围盒 rect cv2.minAreaRect(corners) box cv2.boxPoints(rect) x_coords box[:, 0] y_coords box[:, 1] x_min, x_max min(x_coords), max(x_coords) y_min, y_max min(y_coords), max(y_coords) # 转成 YOLO 格式: class x_center y_center width height均归一化 x_center ((x_min x_max) / 2) / w y_center ((y_min y_max) / 2) / h box_w (x_max - x_min) / w box_h (y_max - y_min) / h txt_path os.path.join(out_dir, base .txt) with open(txt_path, w) as f: f.write(f0 {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}\n)这段脚本的逻辑是先把四角坐标解析成多边形再用cv2.minAreaRect求最小外接矩形最后归一化成 YOLO 格式写入 txt。注意 CCPD 的图片本身就带透视形变直接用四个角点算出的宽高会比手工标注略大一点但对训练影响不大。转换完成后还要把图片路径和 txt 文件按 8:1:1 分到 train、val、test 三个目录YOLO 训练时只认目录结构不认你额外的标注文件。3.2 用 YOLOv8 训练自己的车牌检测数据集yaml 与训练命令数据准备好了接下来写一个数据描述文件。这是 yolov8 训练自己的数据集第一步要改的东西里面指定训练集和验证集路径以及类别数量。# detect/datasets/plate.yaml train: datasets/train val: datasets/val nc: 1 names: [plate]只有一类目标所以 nc 写 1。训练命令用 ultralytics 提供的命令行接口就可以yolo detect train \ datadetect/datasets/plate.yaml \ modelyolov8n.pt \ imgsz640 \ epochs100 \ batch16 \ device0 \ mosaic0.0这里重点说三个参数。第一modelyolov8n.pt用的是 nano 版本车牌检测任务目标单一n 版本完全够用推理速度快部署到边缘设备也轻松没必要一上来就上 yolov8l 这种大模型。第二mosaic0.0要主动关掉mosaic 增强会把四张图拼在一起车牌这种小目标很容易被裁到图块边缘甚至切掉一半导致训练时模型老看到半个车牌。很多人训完发现 mAP 不低但实际视频里漏检严重第一件事就是把 mosaic 关掉再试。第三batch16是按 6G 显存给的参考值如果你用更高显存的卡可以加到 32 或 64但注意 batch 变大后学习率可能要跟着调。训练完成后权重在runs/detect/train/weights/best.pt。判断检测模型好坏不要只看最终 loss要看验证集上的 mAP0.5车牌检测只要这一类目标mAP 0.95 以上才说明框的位置比较可靠。如果 mAP 在 0.9 以下徘徊先回去检查标注框有没有错位再来调超参。3.3 LPRNet 训练数据从哪来合成数据的核心生成脚本LPRNet 识别模型需要大量“车牌图 车牌字符串”的配对数据。真实车牌数据要一张张去采集标注成本高且涉及隐私问题所以常见做法是用合成数据打底。用 OpenCV 把字符画到纯色背景上再随机加透视变换、模糊、噪声和光照变化模拟各种拍摄条件。import cv2 import numpy as np CHARS 京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新 \ ABCDEFGHJKLMNPQRSTUVWXYZ0123456789 def random_plate_text(): # 第一位是省份简称第二位是字母后面是字母和数字混合 province CHARS[np.random.randint(0, 34)] letter CHARS[np.random.randint(34, 58)] tail .join(CHARS[np.random.randint(34, len(CHARS))] for _ in range(5)) return province letter tail def synth_plate(): text random_plate_text() img np.full((60, 180, 3), (200, 40, 40), dtypenp.uint8) # 蓝底BGR for i, ch in enumerate(text): cv2.putText(img, ch, (8 i * 26, 45), cv2.FONT_HERSHEY_SIMPLEX, 1.1, (255, 255, 255), 2, cv2.LINE_AA) # 随机透视变换模拟相机斜拍 h, w img.shape[:2] pts1 np.float32([[0, 0], [w, 0], [0, h], [w, h]]) pts2 np.float32([[np.random.randint(-8, 8), np.random.randint(-5, 5)], [w np.random.randint(-8, 8), np.random.randint(-5, 5)], [np.random.randint(-8, 8), h np.random.randint(-5, 5)], [w np.random.randint(-8, 8), h np.random.randint(-5, 5)]]) M cv2.getPerspectiveTransform(pts1, pts2) img cv2.warpPerspective(img, M, (w, h)) # 缩放到 LPRNet 输入尺寸加高斯噪声 img cv2.resize(img, (94, 24)) noise np.random.normal(0, 5, img.shape).astype(np.uint8) img cv2.add(img, noise) return img, text这段代码里字符集顺序很重要。前 34 个是省份简称接着是去掉 I 和 O 的字母表最后是数字。省份简称、字母、数字的排列顺序在训练和推理时必须完全一致否则模型输出索引和字符对不上整条识别结果全是乱的。OpenCV 的 putText 默认字体不支持中文需要指定系统中文字体文件比如font_path C:/Windows/Fonts/simhei.ttf或 Linux 下的wqy-zenhei.ttc否则汉字直接变成豆腐块合成数据等于白做。合成数据虽然不是真实照片但加上足够强的随机透视和噪声后LPRNet 学到的字符特征可以迁移到真实场景。建议合成 5 万到 10 万张作为训练集再混入几千张真实车牌图微调效果会好很多。3.4 LPRNet 训练命令与损失函数CTC loss 的收敛特征LPRNet 训练脚本的核心是 CTC loss。PyTorch 官方提供了torch.nn.CTCLoss需要三个关键输入模型输出的 log 概率序列、字符索引序列、以及各自的实际长度。车牌是短文本序列长度不长训练起来比长语音识别轻松得多。import torch import torch.nn.functional as F def train_one_epoch(model, train_loader, optimizer, ctc_loss, device): model.train() total_loss 0 for imgs, labels, label_lens in train_loader: imgs imgs.to(device) labels labels.to(device) optimizer.zero_grad() # 模型输出形状: (B, T, C)C 包含一个 blank 位 out model(imgs) log_probs F.log_softmax(out, dim-1).permute(1, 0, 2) # (T, B, C) # 每个样本的模型输出长度都相同用 full 填充 input_lens torch.full((imgs.size(0),), out.size(1), dtypetorch.long, devicedevice) loss ctc_loss(log_probs, labels, input_lens, label_lens) loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(train_loader)训练参数上常见配置是优化器用 Adam初始学习率 1e-3batch size 64 或 128epoch 30 到 50。优化器建议配合ReduceLROnPlateau当验证集损失连续几个 epoch 不降时把学习率降到 1e-4通常能再涨几个点准确率。CTC loss 的一个特点是起步很高前几个 epoch 看起来下降很慢这不是模型坏了是 CTC 在学“怎么对齐”等对齐关系稳定后 loss 才会快速下降。如果训练了一两千步 loss 还在原地踏步先查字符集映射表是否和之前合成数据用的表完全一致这个环节错位就是灾难。训练完成后的评估指标整串准确率比单字符准确率严格得多。7 位车牌只要错一个字就算这条识别失败所以验证时我习惯同时打印编辑距离和整串准确率两个数前者告诉你平均差几个字后者告诉你实际可用度。4. 推理阶段把两个模型串起来从检测框到车牌字符串4.1 检测推理置信度阈值和候选框过滤训练好的 YOLOv8 模型可以直接用 ultralytics 加载推理但直接把所有框都扔给识别模型是不行的。视频场景里车灯、保险杠、反光物都可能被误检成车牌所以第一步要过滤。from ultralytics import YOLO det_model YOLO(runs/detect/train/weights/best.pt) def get_plate_boxes(frame, conf_thres0.4): results det_model(frame, confconf_thres, imgsz640, verboseFalse) boxes [] for box in results[0].boxes: x1, y1, x2, y2 box.xyxy[0].tolist() w, h x2 - x1, y2 - y1 # 车牌宽高比一般在 2.0 到 5.0 之间过滤明显不合理的目标 if w / h 1.5 or w / h 6.0: continue if w 30 or h 10: continue boxes.append((int(x1), int(y1), int(x2), int(y2))) return boxes置信度阈值 conf 一般设置在 0.35 到 0.5 之间。道闸场景光线可控可以设高一些室外强逆光或夜间场景建议降到 0.3避免漏检。宽高比过滤这个步骤容易被忽略但它能挡掉很多“看着像车牌其实不是”的误检比单纯调阈值更有效。4.2 车牌倾斜怎么办透视校正这块做不好识别全白搭检测框输出的是轴对齐矩形但实际车牌在画面里往往是斜的。直接把这个倾斜区域缩放到 94×24 送进 LPRNet字符会被压扁拉长识别准确率掉得很快。简单场景用旋转校正就够复杂场景必须做透视校正。def crop_and_warp(frame, x1, y1, x2, y2): # 先用矩形框裁剪再在灰度图上做透视校正 roi frame[y1:y2, x1:x2].copy() gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) h, w gray.shape[:2] corners np.float32([[0, 0], [w, 0], [0, h], [w, h]]) # 向外扩张角点模拟车牌在画面中的透视方向 warped_corners np.float32([[0, 0], [w - 1, 0], [0, h - 1], [w - 1, h - 1]]) M cv2.getPerspectiveTransform(corners, warped_corners) warped cv2.warpPerspective(gray, M, (w, h)) # LPRNet 输入是固定宽高统一缩放 plate_img cv2.resize(warped, (94, 24)) plate_img plate_img / 255.0 # 归一化必须和训练时保持一致 return plate_img这里有个细节训练时如果用了灰度图归一化到 0~1推理时也必须同样处理否则模型输入分布不一致精度掉几个点是常事。上面代码里的透视变换是简化版只做了轻微扩张实际项目中更好的做法是在检测阶段同时回归车牌四角点拿到真正的四个角点再做透视校正。如果源码里检测模型只输出矩形框那就靠合成数据里的随机透视增强让 LPRNet 自己对轻微形变免疫。4.3 LPRNet 推理与 CTC 解码贪婪解码怎么把序列变成字符串LPRNet 的推理输出是一串时间步上的概率分布要把它变成最终的车牌字符串需要做 CTC 解码。CTC 的解码规则是先按时间步取概率最大的索引然后合并连续相同的索引最后删除 blank 位。这个顺序不能乱先删 blank 再合并会出问题。import torch recog_model None # 实际为加载好的 LPRNet 模型 blank_idx 74 # 字符类别总数对应的最后一个索引常见实现里 blank 在最后 def decode_ctc(pred_indices, blank_idx): decoded [] prev None for idx in pred_indices: if idx ! blank_idx and idx ! prev: decoded.append(idx) prev idx return decoded def recognize_plate(plate_img, idx_to_char): # plate_img 是 94x24 的归一化灰度图 with torch.no_grad(): out recog_model(torch.from_numpy(plate_img).unsqueeze(0).unsqueeze(0)) pred_indices torch.argmax(out[0], dim-1).tolist() char_indices decode_ctc(pred_indices, blank_idx) return .join(idx_to_char[i] for i in char_indices)这段代码的难点在prev这个变量。CTC 解码要求“合并连续相同索引”和“删除 blank”同时进行遇到 blank 时不能打断前面的字符遇到连续相同字符时只保留一个。车牌里连续两个相同字符比如“苏BB1234”里两个 B不会因为合并规则丢字因为两次 B 之间通常隔着 blank 帧blank 会把它们拆开。解码完成后最好加一道规则校验。车牌第一位必须是省份简称汉字第二位必须是字母后面是字母或者数字。如果输出第二位是数字说明识别大概率错了可以做一次重识别或标记给人工复核。这属于后处理规则成本很低但能拦住不少误判。5. 避坑排查车牌识别系统最常见的 5 类翻车现场5.1 蓝色车牌识别率不错新能源绿牌却频繁漏检现象蓝牌车识别基本正常绿牌车要么检测框没有框出来要么框出来了识别结果乱码。 原因训练数据里绿牌占比太低且绿牌颜色和周围环境更接近检测模型对绿牌的响应本身就弱。 解决在检测数据里单独补充绿牌样本至少占到 20% 以上。识别侧更直接的办法是把 LPRNet 输入改成灰度图逼模型不依赖颜色信息只靠字符形状和纹理做判断。这个调整做完绿牌识别率的提升幅度往往比加数据还明显。5.2 字符“8”和“B”、“0”和“D”老混淆现象单字符识别看概率分布没问题但最终输出的车牌里总有几个固定字符对调。 原因形近字问题加上不同省份车牌字体的差异让模型对相似特征产生了混淆。 解决合成数据阶段多换几种字体不要只用一种系统默认字体黑体、宋体、仿宋混着来。推理阶段加后处理规则车牌第二位必须是字母出现数字就重识别。这个位子是硬约束能让模型在概率接近时偏向字母类。5.3 检测框把车灯、车标当车牌现象画面里没出现误检的车牌但输出端偶尔蹦出一串毫无意义的字符往上看发现检测框落在车灯或车标上。 原因检测模型的正样本充足但负样本不够或者说没有专门的难例挖掘来纠正这类误检。 解决把误检区域截出来标记成背景类混回训练数据重新训练一轮。如果没有精力做负样本集至少把置信度阈值从 0.4 提到 0.5同时保留 4.1 节里那个宽高比过滤双重保险。5.4 摄像头视角太大车牌直接被送进识别模型就乱码现象侧拍或者俯拍角度大的摄像头检测框标得挺准但车牌照出来大部分字符是错的。 原因检测框是正矩形车牌的透视形变没有消除字符已经被压成梯形LPRNet 从没见过这种输入。 解决在合成数据阶段加大透视变换的随机范围让模型见过各种形变。推理链路里必须加透视校正步骤不要只做简单 resize。如果做不到四点校正至少用cv2.minAreaRect拿旋转角度做仿射旋转先把车牌扶正再缩放。5.5 整体识别准确率上不去单个字符看着又没问题现象字符级别准确率 95%但整串车牌准确率只有 70% 左右现场验收很难看。 原因7 位车牌只要错一位就算整条错误字符精度 95% 时整串理论准确率大约是 0.95 的 7 次方算下来确实只有 70%。这是评估口径问题不是模型崩了。 解决评估指标不要只盯单字符准确率用编辑距离和整串准确率双指标。落地上给低置信度结果加一次重识别比如让道闸相机连拍两帧取相同结果作为最终输出。注意以上问题在调试时按“现象 → 原因 → 解决”的顺序排查先确认是检测问题还是识别问题。把中间结果落盘是这里最实用的习惯每张测试图都保存检测框和裁剪后的车牌图出错时一眼就能定位是哪一侧在拖后腿。6. 验证与进阶损失曲线、评估口径和 RK3588 部署路径训练完先别急着部署做一轮系统验证。检测侧看 ultralytics 训练输出目录里的results.png里面有 box_loss、cls_loss、dfl_loss 和 mAP 曲线有人特意写脚本画损失函数曲线图其实这个文件就是现成的训练日志每轮自动更新。识别侧在训练脚本里加上每轮验证集的编辑距离记录我习惯写进同一个 log 文件里方便对比不同 epoch 的 ckpt 哪个更值得留。正式评估时准备一段没参与训练的路口视频跑完整条推理链路统计三个数检测召回率有没有漏框、识别整串准确率字符是否全对、单帧耗时算不算实时。车牌识别系统的验收本质上就是这三项别把 mAP 传得神乎其神mAP 再高也代表不了真实视角下的表现。帧率方面yolov8n 加轻量 LPRNet 在普通 PC 上轻松跑到 30 帧以上瓶颈通常在摄像头采集和后续业务逻辑上。部署到边缘设备时常见路径是先把两个模型都转成 ONNX。YOLOv8 用yolo export modelbest.pt formatonnx即可导出LPRNet 需要自己写 torch 导出脚本。如果目标设备是 RK3588最终还要用 RKNN-Toolkit 把 ONNX 转成 RKNN 格式转换时注意量化精度INT8 量化对车牌这种小目标检测影响比较明显我一般先用 FP16 跑通再试 INT8对比一下 mAP 掉了多少再决定要不要量化。刚开始做这个项目时我也迷信过模型结构改完 head 改进识别率提升一点点后来才发现真正的坑全在输入预处理和后处理规则上。现在跑车牌项目我的习惯是先把置信度阈值、透视校正和字符集映射这三个环节固定下来再去动模型结构顺序反了调再多参数都是白费。希望帮到你。本文还有配套的精品资源点击获取
返回列表