尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于YOLOv11+LPRNet的车牌识别系统完整实现方案

基于YOLOv11+LPRNet的车牌识别系统完整实现方案 简介本资源是一套基于YOLOv11与LPRNet联合架构的端到端车牌识别完整实现方案面向计算机视觉方向的学习者、智能交通系统开发者及深度学习实践者旨在解决传统车牌识别中定位不准、字符分割困难、多格式适配差等核心痛点。资源包共209个文件涵盖23个Python主程序、22个Jupyter Notebook含YOLOv11训练与推理、LPRNet微调、车牌矫正与识别全流程代码、16个预训练.pth模型、90张PNG与27张JPG测试图像以及ONNX导出、CSV结果记录等配套文件整体压缩包大小为148.34MB。已有657人学习下载内容结构清晰支持从数据准备、模型训练、检测识别到结果可视化的一站式复现特别包含车牌透视矫正、多场景测试验证及性能对比分析模块可直接用于智能停车、违章抓拍、出入口管理等实际项目部署。 先说明一下车牌识别这个场景在智慧停车、小区门禁、高速收费、园区管理里都是刚需一套能跑通检测加识别的源码方案放到哪都是能直接落地的生产资料。这次我把整个项目的拆解思路、训练细节、坑点记录全部整理出来项目本身基于YOLOv11和LPRNet的组合检测用YOLOv11识别字符用LPRNet两段式管线在实际项目中非常常用。不管是你要做毕设、参赛还是想接到商业项目里做技术预研这篇都能给你一个可以直接照着抄的完整路径。老读者都清楚我平时分享的东西不喜欢绕弯子直接从技术选型开始讲。1. 整体设计方案与技术选型解析1.1 为什么选择YOLOv11和LPRNet这套组合先说说选型逻辑这一步决定了项目的天花板。车牌识别这个任务表面上看起来简单就是找到车牌 读出来两个步骤但实际落地时会遇到各种现实情况车牌倾斜、光照复杂、小目标、模糊、形变。这些环境干扰因素如果在设计阶段没有考虑清楚后面调试时会被反复折磨。目标检测这块可选方案有传统图像处理、YOLO系列、SSD、Faster R-CNN。我自己做项目这么多年传统图像处理方式边缘检测形态学在简单背景下的确能跑但一旦到了真实停车场那种场景墙面反光、路面标志线干扰、夜间灯光的影响都会让规则直接失效。Faster R-CNN精度够但推理速度在边缘设备上不够看。YOLO系列一直是工程落地的主流选项而且YOLOv11相比之前的版本在检测小目标方面做了不少优化车牌本身在画面里往往就是一个小目标这个提升正好踩在需求点上。字符识别这块我选LPRNet而不是传统的字符分割方案原因是实打实被分割方案坑过。传统思路是先做车牌字符分割再逐个字符分类听着简单但车牌上有铆钉、边框、间隔符这些都会干扰分割结果而且遇到倾斜、模糊的情况分割错误率就会爆炸。LPRNet是一条序列识别路线不需要先切分字符直接把整张车牌图片送进网络通过CNN提取特征RNN建模序列CTC损失函数对齐一步到位鲁棒性高得多。简单说LPRNet把这个字符边界在哪的问题简化成了这一串字符是什么顺序容错空间大很多。1.2 整体技术架构与工作流程整个系统的流水线分成两个阶段第一个阶段是检测第二个阶段是识别串联起来就构成了完整的应用。检测阶段输入一张完整图片使用YOLOv11模型定位出车牌区域输出车牌边界框坐标。这一步的输出质量非常关键如果检测框不准确比如把部分背景或相邻区域也框进来了后续识别阶段会受到很大干扰实测下来框体如果再往外扩一点或者倾斜角度没包含完整识别准确率直接下降好几个点。识别阶段把检测阶段裁剪出来的车牌区域图片传入LPRNet网络LPRNet直接输出一个字符序列比如苏E5B6L8这样一个字符串作为最终识别结果。模型训练上YOLOv11和LPRNet是分开训练的。YOLOv11负责学习什么是车牌这个视觉概念LPRNet负责学习车牌上的字符怎么排列。这种解耦设计的好处是任何一方模型需要升级或替换都不会影响另一方的稳定性方便后续迭代。同时这两部分的训练数据也可以分别优化检测模型需要多场景的图片识别模型需要各种像素质量的车牌图两者的增强策略可以独立调节这对于数据工程来说是很大的便利。2. 环境配置与依赖准备2.1 软硬件环境配置清单这一步很多人不重视但在实际跑项目时环境问题往往会浪费一到两天时间。直接把我验证过的配置放出来。我自己测试用的是一张RTX 3090 24G显卡模型训练完全够用推理阶段只用CPU也能跑只是速度会慢一些对实时性要求不高的场景问题不大。推荐的软件环境版本依赖组件推荐版本说明Python3.8 或 3.10两个版本都试过3.8更稳定3.10也兼容PyTorch1.13 或 2.02.0版本训练速度更快对CUDA优化更好CUDA11.7 或 11.8需要和PyTorch版本对应的编译版本一致ultralytics最新版即可YOLOv11官方推理、训练框架包OpenCV4.5.4图像处理基础库numpy1.21数值运算注意和Python版本兼容在Anaconda环境里安装指令我直接给出来依次执行即可conda create -n plate_recognition python3.8 conda activate plate_recognition pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics pip install opencv-python numpy注意PyTorch安装时一定要指定CUDA版本如果直接默认安装可能装成CPU版本训练速度会慢到你怀疑人生。安装完成后用python -c import torch; print(torch.cuda.is_available())验证一下输出True才说明GPU环境正确。2.2 项目目录结构与源码组织方式做项目一定要从一开始就规划好目录结构不然后面代码越写越多会变得难以维护。我常用的组织方式是把检测和识别分成两个独立模块再在入口处串联起来结构清晰方便调试。plate_recognition/ ├── config/ │ ├── detect.yaml # 检测模型训练配置 │ └── lprnet.yaml # 识别模型训练配置 ├── data/ │ ├── detection/ # 检测模型的数据集按YOLO格式组织 │ │ ├── images/ │ │ ├── labels/ │ │ └── trainset.txt │ └── recognition/ # 识别模型的数据集 ├── models/ │ ├── yolo_plate_detector.py # YOLOv11检测封装 │ ├── lprnet_model.py # LPRNet网络定义 │ └── lprnet_trainer.py # LPRNet训练脚本 ├── inference.py # 端到端推理脚本 └── weights/ ├── detect_model.pt # 训练好的检测权重 └── lprnet_model.pth # 训练好的识别权重一个经验是识别模块的数据集目录下建议按省份或字符类型建子目录方便后续增加数据时保持条理。不要小看这个习惯数据规模到几万张的时候目录混乱会让你连统计类别分布都很难受。3. 车牌检测模块YOLOv11核心实现3.1 YOLOv11网络结构核心变化YOLOv11是YOLO系列的一次重要更新相比前面的版本它的骨干网络对特征提取进行了重新设计。采用了更深层但更高效的卷积模块同时引入改进的注意力机制模块C2PSA这个模块的本质是让网络在提取特征时更关注图像中的重要区域。放在车牌检测场景里就是让模型自动聚焦到画面中那些看起来像车牌的区域。它不是一个简单的特征选择而是通过空间注意力加权在多个尺度上融合信息这样从大场景里找到小目标的能力会比前代更强。CSPCross Stage Partial结构的优化也让模型在保持轻量化的同时能更好地学习到细节特征。这些改进最终体现在YOLOv11n这个最轻量版本在推理速度上非常快单张图片在GPU上能达到毫秒级别而且精度损失也不大。这对我这种需要部署到边缘设备的场景很友好。3.2 数据集准备与标注YOLOv11的训练数据需要的是标注好车牌边界框的图片。官方公开数据集里我主要用CCPDChinese City Parking Dataset这是目前国内用得最多的公开车牌数据集采集自真实停车场场景包含多种光照条件、天气和拍摄角度大概有20万张图片。如果要做特定项目比如某城市特定的车牌样式或者特殊场景建议在官方数据集基础上补充自己的数据。标注格式是YOLO格式的txt文件每一行表示一个目标包括类别索引和归一化后的边界框坐标。下面是一个标注文件的示例类别0表示车牌0 0.523437 0.578326 0.238281 0.095421这行的含义是类别0边界框中心点x坐标为图片宽度的0.523437中心点y坐标为图片高度的0.578326边界框宽度占图片宽度的0.238281高度占图片高度的0.095421。这种归一化表示的好处是无论输入图片尺寸怎样变化标注文件都不需要修改。数据增强我是用ultralytics库内置的策略。默认会产生随机翻转、缩放、颜色空间变换等效果。有一个细节需要特别说明车牌是文字信息左右翻转会改变字符顺序所以如果做翻转增强建议只开启上下翻转或者接受左右翻转带来的语义错误。实际情况中我关闭了左右翻转这样能避免模型学到颠倒的字符顺序。3.3 YOLOv11训练配置与参数选择ultralytics框架把训练过程封装得比较友好但参数需要自己调对。我使用的训练参数如下from ultralytics import YOLO model YOLO(yolo11n.pt) # 加载预训练权重 model.train( dataconfig/detect.yaml, # 数据集配置 epochs100, # 训练轮数 imgsz640, # 输入图片尺寸 batch16, # 批次大小 lr00.001, # 初始学习率 device0, # 指定GPU设备 patience15 # 早停轮数 )这里有几个参数值得深究。imgsz我用的640这个值在速度和精度之间比较均衡如果车牌在画面中特别小可以把尺寸提高到960或1280这对小目标检测有明显帮助但训练显存占用和推理耗时都会增加需要权衡。batch设置要根据显存来调整24G显存跑16很稳如果显存不足可以降到8或4同时适当降低学习率。lr0学习率初值用0.001这是ultralytics给YOLO系列训练比较合理的默认值。如果训练时loss曲线出现震荡说明学习率偏高可以降一半试试。patience15表示连续15个epoch验证集指标没有提升就直接停止训练这个机制能节省大量时间。我实际训练时大概跑到60-70个epoch就触发了早停说明模型收敛得比较快。3.4 模型推理与检测结果保存训练完成后模型推理代码非常简单。这里也顺便解决一个高频搜索场景就是如何保存推理结果。ultralytics框架直接支持保存标注后的图片和视频。from ultralytics import YOLO model YOLO(weights/detect_model.pt) results model.predict( sourcetest_images/, saveTrue, # 保存标注后的图片 save_txtTrue, # 保存检测结果坐标到txt文件 conf0.5, # 置信度阈值低于这个值的检测框会被过滤 imgsz640 )保存的结果会自动生成一个runs/detect/predict目录里面包含每张图片的标注可视化结果以及每个检测框的坐标文件。这个功能比较实用因为它能直接把YOLO输出的坐标信息落盘后续喂给LPRNet时不需要自己编代码保存坐标。如果要从视频里检测把source改成视频文件路径即可框架会自动处理视频帧。置信度阈值conf我建议设在0.5附近这个值如果设得太高比如0.8会把一部分模糊车牌过滤掉漏检增加设得太低比如0.3会出现大量误检框把车身贴纸、反光区域都当车牌拉进来进而污染识别模块的输入。4. 车牌字符识别LPRNet模型原理与实现4.1 LPRNet核心网络结构解析LPRNet的全称是Lightweight Deep Network for License Plate Recognition专门为车牌字符识别设计的轻量级网络思路很巧妙。它的设计出发点就是不走字符切分的老路直接对整张车牌图片做序列识别。网络主干是一个轻量级的CNN负责从输入的车牌图片中提取视觉特征中间层加入一个双向RNN我用的实现里是双向LSTM负责建模字符之间的上下文依赖关系。这个结构可以类比成一个人读一长串文字不会一个个字孤立地看而是会结合前后文判断当前字符是什么。车牌字符虽然有固定顺序但不同位置的字符特征会有差异双向RNN能同时利用前后文信息把这个优势发挥出来。在CNN输出特征后LPRNet不是直接接全连接分类层而是把特征图按宽度方向展开成时间步每个时间步对应特征图上一个水平位置这个设计是为了配合CTC Loss做序列对齐。最终每个时间步会输出一个字符概率分布。我使用的网络结构参数如下PyTorch实现的核心部分import torch import torch.nn as nn class SmallBasicBlock(nn.Module): def __init__(self, in_channels, out_channels): super().__init__() self.conv nn.Conv2d(in_channels, out_channels, kernel_size3, stride1, padding1) self.bn nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) def forward(self, x): return self.relu(self.bn(self.conv(x))) class LPRNet(nn.Module): def __init__(self, class_num, dropout0.5): super().__init__() self.backbone nn.Sequential( nn.Conv2d(3, 64, kernel_size3, stride1, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride1, padding1), SmallBasicBlock(64, 128), nn.MaxPool2d(kernel_size3, stride2, padding1), SmallBasicBlock(128, 256), SmallBasicBlock(256, 256), nn.MaxPool2d(kernel_size3, stride2, padding1), SmallBasicBlock(256, 512), SmallBasicBlock(512, 512), nn.Dropout(dropout), ) self.rnn nn.LSTM(512, 256, bidirectionalTrue, num_layers2, batch_firstTrue) self.linear nn.Linear(512, class_num) def forward(self, x): x self.backbone(x) b, c, h, w x.size() x x.permute(0, 3, 1, 2).contiguous().view(b, w, c) x, _ self.rnn(x) x self.linear(x) return x注意class_num这个参数它等于字符集总长度加1多出来的一个位置是CTC Blank符号表示空白。字符集内容在下一小节讲。4.2 字符集定义与标签编码字符集是LPRNet输出层的维度依据。国内车牌通常包含汉字、大写英文字母和数字我把字符集定义为CHARS [ 京, 沪, 津, 渝, 冀, 豫, 云, 辽, 黑, 湘, 皖, 鲁, 新, 苏, 浙, 赣, 鄂, 桂, 甘, 晋, 蒙, 陕, 吉, 闽, 贵, 粤, 青, 藏, 川, 宁, 琼, A, B, C, D, E, F, G, H, J, K, L, M, N, P, Q, R, S, T, U, V, W, X, Y, Z, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]有两处需要注意英文字母里没有I和O因为这两个字母容易和数字1和0混淆这是一个公开的常识规则汉字部分我覆盖了31个省份的简称如果项目只针对某个区域可以适当缩减但建议保留完整字符集方便后续扩展。标签编码就不走one-hot了每个字符直接映射成它在字符集中的索引。举个例子苏E5B6L8编码后变成向量[13, 31, 46, 35, 40, 49, 33]。这里数字和字母在字符集中的索引需要对应好不要出现偏移否则训练出来的模型预测结果会全部错位这个问题排查起来很费时。4.3 CTC Loss训练原理与实现LPRNet训练的核心在于CTCConnectionist Temporal Classification损失函数。这个损失函数设计的初衷就是解决输入序列和输出序列长度不一致的问题。结合车牌识别来理解输入的车牌图片经过特征提取后得到的是按宽度切分的特征序列比如宽度94像素的车牌图片经过下采样后会产生24个特征时间步但车牌字符最多只有7到8个。CTCLoss的任务就是在这24个时间步和7个字符之间建立对应关系允许同一个字符连续重复输出也允许预测输出中插入空白符号。训练时模型对每个时间步都预测一个字符概率分布然后CTCLoss计算所有可能的对齐方式的概率和并与真实标签做对比。优化目标就是让所有可能的合法对齐方式的总概率最大化。这个机制让模型不需要知道每个字符的具体边界在哪只要能正确读出整串字符即可这就是它能容忍字符粘连、边界模糊的根本原因。训练脚本核心代码如下import torch.nn.functional as F def train_one_epoch(model, dataloader, optimizer, device): model.train() total_loss 0 for images, labels, label_lengths in dataloader: images images.to(device) logits model(images) # shape: (batch, time_steps, num_classes) # 转换为CTC Loss所需格式 log_probs F.log_softmax(logits, dim2) log_probs log_probs.permute(1, 0, 2) # (time_steps, batch, num_classes) input_lengths torch.full( size(images.size(0),), fill_valuelog_probs.size(0), dtypetorch.long ).to(device) loss F.ctc_loss( log_probs, labels, input_lengths, label_lengths, blanklen(CHARS), reductionmean ) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(dataloader)blanklen(CHARS)刚好指定了CTC空白符号的索引也就是字符集的最后一个位置。input_lengths统一设为时间步总数因为batch内每张图经过相同网络输出序列长度是一致的。训练时的数据加载挺关键对车牌图片要做归一化处理将像素值缩放到0到1之间并把标签长度单独记录。我在实现里用了DataLoader的collate_fn自定义函数来处理变长的标签序列简单做法是把标签pad到batch内最大长度同时记录每个真实长度这样CTCLoss能通过label_lengths参数知道哪些是有效标签。4.4 LPRNet推理解码与后处理推理阶段模型输出的是每个时间步在字符集上的概率分布需要经过解码转换成最终的字符串。最常用的是贪心解码策略取每个时间步概率最大的字符作为预测结果然后折叠连续的重复字符最后删除空白符号。我用一个例子来说明。假设模型对一张苏E5B6L8的车牌图片输出了14个时间步的预测序列解码过程如下取每个时间步最大概率字符得到原始序列苏 苏 E E 5 5 B 6 6 L L 8 8 _合并相邻重复字符苏 E 5 B 6 L 8 _删除空白符号苏E5B6L8这里合并重复字符是CTC解码的关键规则因为一个真实字符可能横跨多个时间步连续输出需要合并。但要注意如果同一字符在标签中连续出现两次CTC在解码时会引入一个空白符号隔开这样才能保留两个相同字符。推理代码实现如下def decode_greedy(logits, chars): # logits shape: (time_steps, num_classes) pred_indices logits.argmax(dim1).cpu().numpy() result [] prev None for idx in pred_indices: if idx ! prev and idx ! len(chars): result.append(chars[idx]) prev idx return .join(result)这个实现里有一个易错细节只有在当前索引与上一个不同时才会加入结果但合并前需要先判断是否为blank。我的写法通过idx ! prev自然实现了重复合并同时用idx ! len(chars)排除blank。这两步顺序不能反否则逻辑会错。5. 端到端串联检测与识别的完整融合5.1 完整推理流程设计把检测和识别串联起来时需要设计好各模块的接口保证数据流清晰、便于调试。我的推理流程分五步第一步读取原始图片检查图片合法性避免空指针问题。第二步YOLOv11检测得到车牌候选框置信度过滤。第三步对每个候选框进行坐标修正。这一步很关键我会把检测框向外扩展15%的宽度和高度确保完整的车牌字符都在框内特别是边缘的汉字和两侧边框如果不扩展车牌最左侧的汉字经常被截掉一半直接影响识别精度。第四步将裁剪后的车牌区域resize到LPRNet期望的输入尺寸我用的输入尺寸是宽94、高24这个尺寸是LPRNet原文的推荐设置实际效果很好。第五步LPRNet推理并解码输出车牌字符串。坐标修正有一个细节扩展不能超出原图边界需要做clip操作。如果原图中车牌紧贴图片边缘扩展后越界直接clip回边界即可这时代价是可能损失部分字符信息但这种场景本身属于极难样本后续可以通过数据增强专门模拟边缘车牌来覆盖。5.2 关键代码实现与调用示例下面给出融合推理的完整代码。这个代码是项目可运行的最小闭环保留了调试信息输出方便定位问题。import cv2 import torch from ultralytics import YOLO from models.lprnet_model import LPRNet class PlateRecognizer: def __init__(self, detect_weights, lprnet_weights, devicecuda): self.device device self.detector YOLO(detect_weights) self.model LPRNet(class_numlen(CHARS)).to(device) self.model.load_state_dict(torch.load(lprnet_weights, map_locationdevice)) self.model.eval() self.img_width 94 self.img_height 24 def __call__(self, image): results self.detector.predict(sourceimage, conf0.5, verboseFalse) boxes results[0].boxes.xyxy.cpu().numpy() plates [] for box in boxes: x1, y1, x2, y2 box w, h x2 - x1, y2 - y1 # 扩展15%防止边缘字符被截断 x1 max(0, x1 - 0.15 * w) y1 max(0, y1 - 0.15 * h) x2 min(image.shape[1], x2 0.15 * w) y2 min(image.shape[0], y2 0.15 * h) plate_img image[int(y1):int(y2), int(x1):int(x2)] plate_img cv2.resize(plate_img, (self.img_width, self.img_height)) plate_img cv2.cvtColor(plate_img, cv2.COLOR_BGR2RGB) plate_img torch.from_numpy(plate_img).float().permute(2, 0, 1).unsqueeze(0) plate_img plate_img / 255.0 plate_img plate_img.to(self.device) with torch.no_grad(): logits self.model(plate_img) logits logits.squeeze(0) plate_text decode_greedy(logits, CHARS) plates.append({box: box, text: plate_text}) return plates if __name__ __main__: recognizer PlateRecognizer( detect_weightsweights/detect_model.pt, lprnet_weightsweights/lprnet_model.pth ) img cv2.imread(test_car.jpg) results recognizer(img) for res in results: print(f车牌: {res[text]}, 位置: {res[box]})有一个小的性能细节值得注意YOLOv11的predict方法默认带预处理传入的image如果是numpy数组格式框架会自动处理这里的verboseFalse参数可以关掉推理日志在批量处理时避免终端输出刷屏。5.3 批处理与实时视频流场景扩展如果要做视频或实时流的识别检测识别的串联方式需要考虑到性能优化。我用过两种方案实测效果都不错。方案一帧间隔策略。对于帧率25fps的视频流不需要每帧都做检测和识别可以每隔3帧识别一次中间帧直接沿用上一次的检测结果或直接跳过。这样能大幅降低计算负载。如果车在画面中移动不快间隔3帧的结果几乎看不出差别。方案二检测与识别分别调参。检测阶段用轻量级模型yolo11n识别阶段保持LPRNet不变整体单帧处理时间可以从100毫秒降低到30毫秒左右能基本满足实时性要求。如果还想更快可以尝试TensorRT加速推理但环境配置会复杂一些需要单独做模型转换。批量图片处理时可以先对一批图片统一做YOLOv11检测把所有检测到的车牌区域收集起来再一次性输入LPRNet推理。这种批处理方式能充分利用GPU并行能力吞吐量比逐张处理高很多。实现时注意每张图片检测出的车牌数量可能不同需要对list做pad。6. 常见问题与排查技巧实录6.1 检测阶段常见问题检测模型训练完最常见的两个问题漏检和误检。漏检的典型表现是车辆在画面中但检测不到车牌或者检测置信度很低。排查思路是先看测试图片里车牌的目标尺寸很多漏检问题的根源是车牌在640缩放后只有十几像素宽太小以至于特征提取不到。解决办法有两种一种是提高imgsz到960甚至1280另一种是更多地在训练数据中加入包含小尺度车牌的样本数据分布不平衡是模型对小目标不敏感的重要原因。误检的典型表现是把车身广告、车灯、倒车镜里的反光区域识别成车牌。这时需要检查训练数据中负样本是否充足。YOLO训练数据里除了包含正样本图片也应该加入大量没有车牌的背景图片这些图片没有标注文件模型会从中学习什么是不存在的目标。我做过一个实验在训练数据中混入30%的纯负样本误检率可以下降到原来的三分之一左右。还有一个容易忽略的问题就是检测框抖动。在视频流中如果检测框在相邻帧之间跳动很大对识别稳定性影响很明显。解决思路是加一个简单的卡尔曼滤波或平滑算法对检测框坐标做时间维度的平滑我用了指数移动平均就能获得很好的效果。6.2 识别阶段常见问题识别错误的表现更多样我这里把典型情况全部列出来。车牌每个位置的字符都可能出错但规律不一样。苏被识别成京大概率是汉字样本不均衡训练集里江苏车牌数量远多于其他省份模型形成偏置。解决办法是均衡各类样本数量特别是各省汉字字符最少一类也要保证1000张以上这个数字我实测过低于500张的类别准确率会明显下降。数字和字母混淆的情况如O和0、B和8这类错误是模型在视觉特征无法区分时的自然结果。LPRNet的字符集里已经有部分规避方案但字符图片本身模糊时单靠模型难以彻底纠正。工程上还可以加一个后处理规则根据车牌第位字符的合法范围做约束比如车牌第二位固定是字母第五位之后通常是数字或字母如果模型输出违法了字符串模式规则就对概率较低的候选做修正。车牌倾斜角度过大时LPRNet的准确率会下降。虽然LPRNet本身对倾斜有一定的容错能力但超过15度时还是建议在输入到识别网络前做一次仿射变换矫正。这个操作可以通过OpenCV在推理前完成或者用一个轻量级的关键点检测模型来估算车牌的边角再进行透视变换。实测中矫正后的倾斜车牌识别准确率能从85%左右提升到97%以上效果显著。6.3 环境与工程化部署问题环境问题最常见的是CUDA版本不匹配表现是导入torch时报错找不到CUDA驱动或者训练时提示DLL加载失败。这个问题通常是把PyTorch的CUDA版本和本机驱动版本对应上就能解决注意本机显卡驱动版本只需要不低于CUDA版本要求即可不需要降级驱动。内存溢出问题也很常见在训练LPRNet时如果batch size设置过大可能报CUDA out of memory。解决方式除了减小batch外还可以在训练循环中增加一点梯度累积策略用小batch累积梯度再更新参数效果等价于大batch训练。还有一个小坑使用OpenCV读取图片后图片颜色通道是BGR而PyTorch训练时用的通常是RGB。如果推理时忘记转换模型看到的颜色整体偏蓝识别准确率会明显下降。这个问题调试时很难发现因为只看坐标信息是看不到颜色变化的需要额外打印预测概率对比才能定位到。我在推理代码里已经加入了cv2.cvtColor转换建议各位在自己实现时也加上。6.4 识别结果置信度与性能分析落地部署时需要有一个稳定可靠的置信度标准。我在实践中的经验是检测置信度加识别置信度两个乘积可以作为最终车牌识别结果的可信度指标设定阈值0.8以上算高置信度0.5到0.8之间算中置信度需要人工复核场景可以只关注中低置信度区域。这样就不用在海量图片里逐张检查能大幅减少人力成本。性能方面给出一个参考在RTX 3090上YOLOv11n加LPRNet整体推理一张1080P图片耗时约25到35毫秒。在CPU上单张图片约300到500毫秒。如果在Jetson这样的边缘设备上部署可以用TensorRT做加速性能通常在30毫秒以内完全满足停车场出入口的实时需求。内存占用方面两个模型加起来不到200MB部署非常轻量。我在实际项目中发现一个很有意思的现象夜间场景的车牌识别准确率并不一定比白天低太多因为车牌本身有反光涂层夜间相机抓拍时红外补光或闪光灯可以增强车牌区域的可读性。真正难的是逆光或者阴影遮挡场景这种情况下车牌的对比度很低字符和背景融为一体。处理这类难点除了数据层面补充更多极端光照样本也可以在预处理时做自适应直方图均衡化增强局部对比度后再送入识别网络我测试过这种方法在逆光场景下能提升5到8个百分点的识别率。7. 训练数据构建与性能提升策略7.1 数据规模、标注质量与增强策略训练数据是这条技术路线成功与否的关键有时候比模型结构的影响还大。我总结一下自己踩出来的数据工程经验。检测模型的数据量建议不低于5000张真实场景图包含车牌的各种位置、尺度和天气条件CCPD数据集本身质量很高但如果项目要落地到特定场景最好加入自己拍摄的场景数据。识别模型的数据量则更讲究类别均衡每个字符至少1000张样本总样本量建议2万到5万张。我之前做过一轮实验把数据量从1万张加大到4万张后识别准确率从91%左右提升到97%以上投入产出比非常高。标注质量上有一个容易被忽略的细节检测框是否紧贴着车牌的边缘。建议标注时让边界框紧贴车牌边缘不要把车头的其他部位包进来因为多余背景会干扰识别网络的特征提取同时影响检测框的稳定性。数据增强我按重要性排序推荐以下几种策略优先使用高斯模糊模拟车牌在运动或低焦距情况下的模糊效果高斯噪声模拟光照不足时摄像头的高ISO噪点随机亮度对比度调整模拟早晚阳光、地库灯光的变化随机旋转正负10度模拟车辆行驶中的倾斜水平拉伸压缩模拟不同拍摄角度引起的形变不过需要再次强调左右翻转增强就不要开了车牌字符顺序会因此变得混乱。另外增强强度不宜过大过度模糊和噪声会让模型学习到偏离真实分布的特征反而影响正常场景的识别效果。7.2 从数据到精度提升的层次策略如果模型精度达不到要求我建议按照下面的优先级做优化而不是一上来就换更大的模型第一步先扩充数据。低置信度样本收集并人工标注这是最稳定可靠的方式。第二步调整数据增强策略。如果模型对模糊鲁棒性差就增强模糊类样本的比例。第三步优化预处理环节。加入检测框自动纠正、图像矫正等算法。第四步使用模型集成如YOLOv11s和yolo11n的检测结果取并集LPRNet同时训练两个不同随机种子的模型预测结果做投票一般能提升1到2个百分点但推理耗时也会相应增加。第五步才是考虑更换更大的模型结构比如YOLOv11m以及更大尺寸的LPRNet变体。精度提升过程中还需要密切关注过拟合信号。如果训练集loss持续下降但验证集loss在某个epoch之后开始上升说明模型过拟合了。此时需要增加数据量或增强程度或者调低模型复杂度。从具体的训练log里看正常训练下训练集准确率和验证集准确率应该保持同步趋势差距过大的情况就要警惕。7.3 两个模型的联动调优检测和识别是两个独立训练的网络但串联使用后会有相互影响的联动调优空间。检测框的边界质量直接影响识别结果。检测阶段输出的框如果与真实车牌区域有偏移比如车牌的左边界没有完全覆盖那识别结果中最左侧汉字大概率会出错。所以在最终调优阶段我会用识别准确率作为检测模型效果的最终评价指标而不是只看检测mAP。这样做看起来绕了一圈但实际上更贴合真实目标。具体做法是用检测模型输出候选框裁剪后送识别模型计算端到端识别准确率以这个指标来调节检测阶段的置信度阈值和NMS参数通常能发现更合理的参数组合。识别阶段还可以利用一个先验知识车牌的字符模式。国内民用车牌的标准格式是1位汉字1位字母6位字母或数字在LPRNet输出结果后可以增加一个规则校验层对不符合此模式的识别结果进行修正或降权。我在实际项目中加入这个规则后整体准确率提升了0.5个百分点关键是减少了一些看起来很合理但实际错误的结果这类结果对用户信任度的破坏比直接报错更严重。关于车牌的类型判断LPRNet本身只负责字符识别但实际项目中常常需要区分不同车牌类型。比如新能源车牌是8位字符比普通蓝牌多一位警车车牌可能是白底使馆车牌有特殊标识。工程上建议在检测阶段就增加一个车牌类型分类分支或者直接多训练一个简单的分类器根据车牌颜色区域特征做类型判断。颜色特征用HSV空间统计即可绿色区域占比高就是新能源绿牌蓝色占比高就是普通蓝牌。这个方法简单有效不需要额外训练深度学习模型。最后再分享一个我个人在调试过程中的心得车牌识别这类任务模型的标准答案其实是固定的车牌号不会出现模糊的语义空间。所以任何一个识别错误都是值得深挖的不应该轻易归咎于模型性能不够。大部分错误的根源要么是训练数据覆盖不足要么是预处理链路有缺陷要么是检测框质量拉胯。只要按这几个方向逐一排查精度是能一步步稳定提升的。我这个项目目前已经把YOLOv11和LPRNet的完整源码、训练配置和推理脚本都整理归档了后续如果有朋友想扩展到夜间红外场景、多车牌同时识别或者嵌入式设备部署都可以在这个框架上直接做增量开发不用从头再来。本文还有配套的精品资源点击获取
返回列表