资讯详情 基于YOLOv8和LPRNet的车牌识别系统实战:检测与识别分离的完整流程
📅 2026/10/5 5:34:56
简介基于YOLOv8与LPRNet的车牌识别系统内含完整Python源码与预训练模型面向计算机、数学、电子信息等专业学生适合作为课程设计、期末大作业或毕业设计项目参考。系统采用前后端分离结构后端以Flask搭建推理服务前端使用Vue构建操作界面并将YOLOv8目标检测、LPRNet车牌识别、数据集生成及划分等模块整合在一起。资源包共60个文件包含13个Python脚本、22张样例图片、3个pt及1个pth模型文件另有Vue前端页面、说明文档等压缩包整体约36.15MB目录层次分明方便按需取用。目前已有1171人学习下载。借助该资源学习者不仅可获得可直接运行的车牌识别Demo还能从数据预处理、模型训练到服务部署完整走通一套流程相关生成与划分脚本也有助于二次开发或算法复现。1. 车牌识别系统别急着端到端先分清检测和识别两件事车牌识别系统在停车场出入口、路侧监控、高速收费这些场景里已经不算新东西了但真正落地的方案大多不是“一个模型吃进图片吐字符”而是两段式流水线先用一个目标检测模型把车牌在画面里的位置框出来再用一个序列识别模型把框里的字符读出来。基于 YOLOv8 和 LPRNet 的车牌识别系统 python 源码模型这套组合走的正是这条路线——YOLOv8 干检测的活LPRNet 干识别的活两者各管一段模型体积和推理速度都控制得住改起来也方便。这套方案尤其适合两类人一类是刚接触车牌识别、想在本地把 demo 跑起来看效果的学生或转行者另一类是已经在用传统图像处理做车牌识别、被复杂场景折磨到想换方案的工程师。你不需要从零训练一个大模型基于开源的 YOLOv8 权重和自己整理的少量车牌数据就能把系统搭起来。下面按我实际做过的流程从数据、训练、串联到踩坑完整讲一遍。2. YOLOv8 做车牌检测数据、训练与网络结构选型2.1 车牌检测为什么选 YOLOv8小目标、多尺度与推理速度的权衡车牌在监控画面里通常只占很小一块区域而且不同卡口的摄像头安装角度、距离差异很大同一辆车在画面里可能从近到远不断变化。传统做法用颜色特征定位蓝色车牌或者用形态学算子提取矩形边缘在固定角度、固定场景下能用但一换场景就翻车。YOLOv8 作为 anchor-free 的检测器对不同尺度的目标天然友好尤其是它的检测头在不同特征层上分别预测小、中、大目标车牌这种小目标落在浅层特征图上也能被召回。选 YOLOv8 而不是更重的 YOLOv5-L 或者两阶段 Faster R-CNN核心原因是推理速度和部署成本。车牌识别系统往往要跑在边缘设备上比如 RK3588、Jetson 这类带 NPU 的板子。YOLOv8n 的权重只有 6MB 左右INT8 量化后更小在 RK3588 上跑一帧 640×640 的检测只需要几十毫秒完全够用。常见的做法是先用 yolov8n 或 yolov8s 做基线如果小目标召回不够再考虑换输入分辨率或者换更大的模型 —— 而不是一上来就上大模型。从网络结构上看YOLOv8 把 C3 模块换成了 C2f颈部 PAN-FPN 结构保留检测头从 anchor-based 改成 anchor-free 的 Decoupled Head。它输出的是每个格子对目标框的四个距离回归值加类别概率不需要像 YOLOv5 那样在损失函数里做 anchor 匹配的复杂逻辑训练和调参都更简单。对于车牌这类类别单一就是“车牌”一个类、长宽比相对固定的目标模型学的压力主要在框的回归精度上类别区分几乎没有难度。2.2 准备车牌检测数据集标注格式、目录结构与数据增强训练 YOLOv8 检测车牌数据集的标注格式是 YOLO 的 txt 格式每行五个值类别索引、中心点 x、中心点 y、框宽 w、框高 h全部归一化到 0~1。如果你的原始标注是 XMLVOC 格式或者 JSONCOCO 格式需要先转换。最常见的做法是从公开车牌数据集 CCPD 或者网上的开源车牌图片出发把它们整理成 YOLO 格式再补充一部分自己场景的抓拍图。一个可用的数据集目录结构如下plate_dataset/ ├── images/ │ ├── train/ │ │ ├── img_0001.jpg │ │ └── ... │ └── val/ │ ├── img_1001.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── img_0001.txt │ │ └── ... │ └── val/ │ ├── img_1001.txt │ └── ... └── data.yamldata.yaml 内容path: /path/to/plate_dataset train: images/train val: images/val names: 0: plate这里有个细节val 目录下的每张图片必须有对应的 txt 标注文件哪怕某张图里没有车牌也要放一个空的 txt 文件否则训练会报错。我在第一次训练时因为漏了几个空标注跑了一会儿突然报 “AssertionError: Label class is greater than number of classes” 或者找不到文件排查了半天。数据增强方面YOLOv8 自带 Mosaic、HSV 扰动、随机翻转、平移和缩放。对于车牌场景我一般会额外关掉或调低水平翻转的概率因为车牌上的字符顺序是有意义的——翻转后“京A12345”会变成镜像检测框虽然还是框但后续 LPRNet 识别会学错方向。在 ultralytics 的配置里把fliplr从 0.5 调低到 0.1 比较合适。另外送高degrees旋转增强到 10 到 15 度左右模拟停车场的各种倾斜角度。2.3 训练 YOLOv8 检测模型命令、损失曲线与必调参数在本地跑通训练的最小命令是yolo detect train \ data/path/to/plate_dataset/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ patience20 \ projectruns/plate_detect \ nameexp1 \ fliplr0.1逻辑说明modelyolov8n.pt的意思是加载 COCO 预训练权重继续微调而不是从零初始化随机权重。对于车牌这种和 COCO 类别差异很大的领域预训练权重依然能提供边缘纹理和物体形状的基础特征收敛速度明显更快。patience20表示 20 个 epoch 内验证集指标没有提升就提前停止避免盲目跑满 100 轮浪费时间。参数说明imgsz建议在 640 和 1280 之间取舍。输入越大小目标检测效果越好但显存占用和推理耗时同步上升。如果你的摄像头画面里车牌普遍很小先试 640 看召回情况不行再加到 1280不要一上来就大图。batch取决于显存大小16G 显存跑 yolov8n 可以开到 32 甚至更大显存不够就减半。训练结束后看runs/plate_detect/exp1/results.csv里的mAP50-95曲线。车牌是单类目标mAP50 达到 0.95 以上才算合格mAP50-95 能到 0.8 基本就够用。如果 mAP50 只有 0.8 不到先检查是不是数据量太少或者标注质量有问题而不是急着调网络结构。我见过有人拿几百张图训练后识别率不稳定加了数据增强、换了更大的模型都不见好最后发现是标注框把车牌边框多包了一圈把周围的车身也包进去了导致 LPRNet 后续裁剪进来一堆噪声。还有一个很容易忽略的点验证集的数据分布要和你实际部署场景一致。如果你用的是网上公开数据训练验证集也是同一来源的随机切分指标会虚高。我一般会单独留一个自己拍的、或者模拟目标场景的图片夹做 hold-out 验证以这个结果为准来判断泛化能力。3. LPRNet 识别模型从字符序列到 CTC 损失函数3.1 LPRNet 网络结构为什么它能做到轻量且支持不定长识别LPRNet 是俄罗斯团队提出的轻量车牌识别网络核心设计是小参数量加序列识别。它没有用 RNN 做时序建模的复杂实现而是用 CNN 提取特征再用一个宽卷积配合一个可选的 RNN 层做序列上下文建模最后用 CTCConnectionist Temporal Classification损失函数来对齐不定长字符序列。车牌字符数量不是固定的——蓝牌是 7 位新能源牌是 8 位教练车、使馆车也不一样——所以不能用固定类别的分类头直接输出。CTC 恰好能处理输入序列和输出标签序列长度不一致的问题。典型的 LPRNet 主网络结构是这样输入一张 24×94 的灰度车牌图经过若干卷积层逐步把空间维度缩小同时通道数增加在特征图宽度方向保留时序维度每列特征代表一个时间步把这些时序特征送进分类头输出 T×num_classes 的概率矩阵最后由 CTC 解码出最优字符路径。我在 PyTorch 里实现过一个简化版 LPRNet核心结构如下import torch import torch.nn as nn class SmallLPRNet(nn.Module): def __init__(self, num_classes68): super().__init__() # CNN backbone: 输入 1x24x94 (灰度) self.conv1 nn.Sequential( nn.Conv2d(1, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride2) # 宽高减半 ) self.conv2 nn.Sequential( nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride2) ) # 时序宽卷积: 沿宽度方向聚合上下文 self.conv_wide nn.Conv2d(128, 256, kernel_size(1, 5), padding(0, 2)) self.rnn nn.LSTM(256, 64, bidirectionalTrue, batch_firstTrue) self.fc nn.Linear(128, num_classes) def forward(self, x): # x: (B, 1, 24, 94) x self.conv1(x) # - (B, 64, 12, 47) x self.conv2(x) # - (B, 128, 6, 23) x self.conv_wide(x) # - (B, 256, 6, 23) # 取高度方向均值, 得到一维序列 x x.mean(dim2) # - (B, 256, 23) x x.permute(0, 2, 1) # - (B, 23, 256) x, _ self.rnn(x) # - (B, 23, 128) logits self.fc(x) # - (B, 23, num_classes) return logits # logits 传给 CTC loss逻辑说明卷积部分把 24×94 的输入映射成 256 通道、23 个时间步的特征。这里的“时间步”对应车牌图像从左到右的空间位置——车牌字符是横向排列的所以特征图宽度方向天然可以作为序列长度。LSTM 在这里不是必须的去掉后可以用纯卷积的 CTC 模型但加一个双向 LSTM 能明显提升相邻字符之间的上下文建模代价是推理速度慢一点。参数说明num_classes是你的字符集大小加一个 CTC blank。比如字符集有 67 个字符31 个省份汉字 24 个字母去除 I/O 10 个数字 2 个特殊字符那num_classes就是 68。字符集定义必须和训练数据一致模型推理时标签映射表要存成 json 或者 py 文件部署时和权重一起带上否则没法把类别 ID 翻译回汉字和字母。3.2 车牌字符集与标签编码这是识别系统最容易被低估的环节LPRNet 的分类头输出的是每个时间步上字符的概率分布训练时需要把 GT 字符串编码成标签序列传给 CTCLoss。中文车牌字符集比英文复杂得多省份简称有 31 个汉字京、津、冀、晋、蒙、辽、吉、黑、沪、苏、浙、皖、闽、赣、鲁、豫、鄂、湘、粤、桂、琼、渝、川、贵、云、藏、陕、甘、青、宁、新字母去掉 I 和 O避免和数字 1 和 0 混淆数字 0~9。如果你还要支持使馆牌、警牌、军牌字符集会更多。标签编码的做法是把字符串按字符逐一映射成索引序列比如“京A12345”映射成[23, 10, 1, 2, 3, 4, 5]索引值取决于你定义的映射表。CTCLoss 接受的 target 是一个二维张量(batch, target_length)每个样本的 label 是一个整数序列。空车牌或者识别不出字符的情况在训练时不要混入因为 CTC 只负责对齐不负责教你“没车牌也要有输出”。字符集的编码代码特别简单plate_chars 京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新 \ ABCDEFGHJKLMNPQRSTUVWXYZ0123456789 char_to_idx {c: i for i, c in enumerate(plate_chars)} idx_to_char {i: c for i, c in enumerate(plate_chars)}逻辑说明char_to_idx用于训练时把车牌字符串转成标签序列idx_to_char用于推理时把模型输出转回可读的字符串。这里有个细节char_to_idx 里没有留 blank 的位置CTCLoss 内部约定blank0或blanknum_classes-1取决于你调用时的参数所以要么从索引 1 开始编号要么在 Pytorch 的 CTCLoss 里把blank显式设成你的类数减一。最常见的错误就是字符索引从 0 开始结果 CTC 把真实的字符当成了 blank导致训练起来 loss 一直降不下去。3.3 训练 LPRNet 的关键配置输入尺寸、batch size 与学习率LPRNet 训练数据的来源是检测模块的产物把原始图片里车牌区域裁剪出来统一缩放到 24×94 的灰度图。公共车牌数据集 CCPD 的裁剪已经很规范你也可以从检测模型的训练集里顺带把车牌框裁出来配上标注字符串形成识别训练集。数据量不需要像检测那么夸张几千张高质量车牌图经过数据增强就能训练出一个可用的模型。训练的最小流程分三步加载图片和标签、做在线增强、跑 CTC loss 反向传播。下面是一个简化的训练循环import torch import torch.nn.functional as F def train_one_epoch(model, dataloader, optimizer, device): model.train() total_loss 0 for imgs, labels, label_lens in dataloader: imgs imgs.to(device) # (B, 1, 24, 94) optimizer.zero_grad() logits model(imgs) # (B, T, num_classes) # 转置成 (T, B, num_classes) 供 CTCLoss 使用 log_probs F.log_softmax(logits.permute(1, 0, 2), dim-1) # 每个样本的输入序列长度均为 T input_lens torch.full((logits.size(0),), logits.size(1), dtypetorch.long) loss F.ctc_loss(log_probs, labels, input_lens, label_lens, blank0) loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(dataloader)逻辑说明PyTorch 的F.ctc_loss要求输入 log_probs 的形状是(T, B, num_classes)T 是序列长度B 是 batchnum_classes 是字符类别数加 blank。labels是所有样本的标签拼接成的一维序列label_lens是每个样本的标签长度。这个拼接方式刚上手容易写错——如果你按 batch 维度压成 (B, L) 再展平顺序对了没问题但 label_lens 必须和拼接顺序严格对应。参数说明输入尺寸 24×94 是 LPRNet 原论文的数值也是大多数开源实现的默认值。不要随意改成其他尺寸因为网络的池化层和时序长度是按这个尺寸设计的改了输入尺寸后 RNN 的时间步和卷积的感受野都要重新算。学习率一般从 0.001 开始用 Adam 优化器配合每 10 个 epoch 衰减一次。车牌字符识别对学习率比较敏感我用 0.001 训练时 loss 稳定下降调到 0.01 后 loss 直接震荡到不收敛。4. 把 YOLOv8 和 LPRNet 串成完整系统推理管线与后处理4.1 整体流程与模块划分不要让两个模型互相污染输入单张图片的推理流程分四步检测、裁剪、预处理、识别。检测模块吃全图输出车牌框识别模块只吃裁剪出来的车牌区域。这个流程看起来简单但模块边界要划清楚——如果识别模块的输入里混入大量背景噪声LPRNet 的时序特征会被干扰从而读错字符。一个完整的推理脚本骨架如下import cv2 import torch from ultralytics import YOLO class PlateRecognizer: def __init__(self, det_weights, rec_weights, char_map): self.detector YOLO(det_weights) self.recognizer load_lprnet(rec_weights) # 加载 PyTorch 权重 self.char_map char_map self.rec_imgsz (24, 94) def recognize_plate(self, img): # 1. YOLOv8 检测 results self.detector(img, conf0.5, imgsz640, verboseFalse) plates [] for r in results: boxes r.boxes.xyxy.cpu().numpy() for box in boxes: x1, y1, x2, y2 [int(v) for v in box] # 2. 裁剪车牌区域加一点边缘余量 h, w y2 - y1, x2 - x1 pad_x, pad_y int(w * 0.08), int(h * 0.08) x1 max(0, x1 - pad_x) y1 max(0, y1 - pad_y) x2 min(img.shape[1], x2 pad_x) y2 min(img.shape[0], y2 pad_y) crop img[y1:y2, x1:x2] # 3. 灰度化 归一化 resize crop_gray cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) crop_resized cv2.resize(crop_gray, self.rec_imgsz) crop_tensor torch.from_numpy(crop_resized).float() / 255.0 crop_tensor crop_tensor.unsqueeze(0).unsqueeze(0) # (1,1,24,94) # 4. LPRNet 识别 with torch.no_grad(): logits self.recognizer(crop_tensor) # (1, T, num_classes) text self.decode_ctc(logits) plates.append((box, text)) return plates def decode_ctc(self, logits): probs logits.squeeze(0) # (T, num_classes) preds probs.argmax(dim-1).cpu().numpy() # 去除重复字符和 blank prev -1 result [] for p in preds: if p ! prev and p ! 0: # 0 是 blank result.append(int(p)) prev p return .join(self.char_map[i] for i in result)逻辑说明检测框往外扩 8% 的边距是很有必要的。YOLOv8 输出的框通常紧贴车牌边缘但 LPRNet 的输入需要把车牌边框外侧的一点区域包进来否则边框字符会被裁剪掉一半识别率明显下降。扩边大小要适中——扩太多会把周围车身颜色带进来扩太少又剪掉字符。8% 是我在多个数据集上试过的一刀流参数基本不用再调。参数说明conf0.5是检测置信度阈值。在卡口场景如果漏检比误检更严重把它降到 0.3如果误检太多比如把车标、广告牌当车牌就把它升到 0.7。实际部署时这个阈值要单独调没有万能值。4.2 车牌倾斜与图像几何什么时候必须做透视矫正YOLOv8 输出的是松散的矩形框但车牌在画面里经常有透视变形——从侧面看过去车牌是一个梯形。LPRNet 对轻度倾斜有一定的容忍度但在角度超过 10~15 度时识别率会明显下降。常见做法是加一个透视矫正步骤用车牌的四个角点做变换把梯形拉伸成矩形。OpenCV 的透视矫正实现并不复杂def four_point_transform(gray, points): # points: 四个角点坐标, 顶点顺序: 左上、右上、右下、左下 tl, tr, br, bl points width_top int(np.linalg.norm(np.array(tr) - np.array(tl))) width_bottom int(np.linalg.norm(np.array(br) - np.array(bl))) width max(width_top, width_bottom) height_left int(np.linalg.norm(np.array(bl) - np.array(tl))) height_right int(np.linalg.norm(np.array(br) - np.array(tr))) height max(height_left, height_right) dst np.array([[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]], dtypenp.float32) src np.array(points, dtypenp.float32) M cv2.getPerspectiveTransform(src, dst) warped cv2.warpPerspective(gray, M, (width, height)) return warped逻辑说明cv2.getPerspectiveTransform把源图像中四个点映射到目标矩形的对应位置。角点坐标的来源可以是检测框的四个角也可以通过边缘检测找车牌轮廓的最小外接矩形再取角点。前者简单但不精确——检测框是水平矩形没有车牌的旋转信息后者精度更高但会增加一次轮廓查找的计算开销。参数说明width_top和width_bottom分别是透视梯形上边和下边的像素长度理论上这两个值越接近说明透视变形越小。如果检测框本身就带角度比如用旋转目标检测模型输出带角度的框可以直接把旋转框的四个顶点传进来省去轮廓查找。做矫正前记得先灰度化warpPerspective直接作用于彩色图也可以但后续 LPRNet 反正要转灰度提前转可以少算一遍。4.3 集成推理与性能分析定位瓶颈在检测还是识别系统搭好后最常遇到的性能问题是识别率不达标。这时候不要凭感觉乱调先把问题拆开是检测框不对还是识别模型读错字符。一个可操作的办法是把识别模块的输入图批量保存下来按“检测召回率”和“字符准确率”两个指标分别统计。检测召回率 正确框出的车牌数 / 实际车牌总数。如果这个指标低说明 YOLOv8 没检测到牌照把检测阈值下调并且检查输入图像分辨率是否够大。字符准确率 识别完全正确的车牌数 / 检测到并送入识别的车牌数。如果检测召回率很高但字符准确率低问题出在 LPRNet 这边——有可能是字符集不匹配、训练数据太少也可能是裁剪预处理不对。实际排障时我会打印出每一层的时间import time t0 time.time() results self.detector(img, conf0.5, imgsz640, verboseFalse) t1 time.time() # ... 裁剪和预处理 ... logits self.recognizer(crop_tensor) t2 time.time() print(fdetect: {(t1-t0)*1000:.1f}ms, recognize: {(t2-t1)*1000:.1f}ms)这个打印输出的价值在于如果识别的时间占总耗时一半以上而你的部署环境是嵌入式设备LPRNet 的 LSTM 层往往是瓶颈——LSTM 在 CPU 和轻量 NPU 上的加速效果不如卷积好。此时可以考虑把双向 LSTM 换成一层宽卷积1×7加两个全连接层字符准确率会掉 1~2 个百分点但推理时间能缩短一半多。5. 车牌识别系统避坑指南五个高频翻车现场与解决方案5.1 检测框一直抖动视频里识别结果闪烁不定现象在视频流中部署后同一辆车静止在画面里时检测框的位置和大小每帧都在轻微变化识别结果有时对有时错屏幕上显示的车牌号来回跳。 原因YOLOv8 是单帧推理模型没有帧间关联。车牌的边缘特征和曝光在帧与帧之间有随机噪声导致检测框回归值在小范围内波动。如果识别模块对这个波动很敏感字符就容易被读错。 解决最简单的做法是在绘制结果时对检测框做指数平滑——用上一帧的框和当前帧的框做加权平均。更稳妥的做法是引入 ByteTrack 这类轻量跟踪算法为每辆车分配 ID车牌识别结果按置信度投票同一个 ID 连续几帧都识别出同一结果才显示否则保留上一帧的结果。5.2 汉字老是识别错字母数字却基本没问题现象识别“京A12345”变成了“津A12345”其他省份汉字也常有混淆但字母和数字的准确率在 98% 以上。 原因汉字笔画复杂、结构相似京和津、沪和津LPRNet 的空间特征提取对这些细微差异不够敏感。更关键的是训练数据里各省份车牌数量严重不均衡——如果训练集里“京”字样本有 5000 张“藏”只有 200 张样本少的省份自然更容易错。 解决首先保证训练集各省份汉字数量均衡最少的一类也不低于 1000 张。如果数据不好找可以对含特定汉字的样本做复制加轻微扰动对比度、亮度、旋转来增强。其次在模型层面可以把汉字单独拆一个分类头在特征图高度方向截取上半部分单独做分类这个办法会改网络结构但效果明显。5.3 夜间场景识别率从 95% 掉到 50% 以下现象白天测试一切正常到了晚上识别率断崖式下跌。检查发现 YOLOv8 偶尔漏检LPRNet 即使拿到裁剪图也读不准。 原因夜间车牌区域亮度低、噪点多摄像头自动增益还会让车牌反光过曝。训练时如果以白天数据为主模型在低照度分布上的特征能力很弱。 解决分两步走。第一步是图像预处理层加自适应直方图均衡化CLAHE在灰度化之后、resize 之前做能明显提升暗部车牌对比度。第二步是训练数据里混入夜间图片用合成方式模拟——白天数据降亮度、加高斯噪声、模拟强光遮蔽。如果条件允许专门收集一段时间夜间抓拍数据做二次微调。5.4 导出 ONNX 后推理结果与 PyTorch 不一致现象PyTorch 里识别得好好的模型导出 ONNX 后用 onnxruntime 推理输出的字符概率分布和 PyTorch 差了不少甚至解码出来的字符串错误。 原因LPRNet 里有几个容易踩的算子兼容性雷区LSTM 的动态 shape、mean(dim2)的降维操作、以及 OpenCV 预处理和 PyTorch 预处理的微小差异比如 BGR/RGB 顺序、归一化系数。 解决导出 ONNX 时把输入尺寸固定为(1, 1, 24, 94)不要用动态 batch 和动态尺寸。LSTM 导出时要保证序列长度是固定的onnxruntime 对动态 LSTM 的支持不如静态 shape 稳定。另外要检查 ONNX 模型里是否带BatchNorm的 training 状态——推理模型必须切换到 eval 模式否则 BN 层的统计量会导致输出漂移。5.5 车牌区域被雨刷、遮挡物盖住系统完全傻眼现象雨天雨刷扫过车牌或者车辆加装了遮挡物防虫网、保险杠装饰条检测框面积变小或者完全检测不到。 原因目标检测模型对“不完整目标”的辨识能力有限——训练数据里基本没有遮挡样本。而且遮挡会导致车型特征和车牌特征重叠模型对车牌的置信度大幅下降。 解决短期方案是降低检测置信度阈值到 0.25 左右让概率低一点的框也能跑完整个流程然后靠识别模块的分数做二次过滤——如果识别结果置信度也低就丢弃不显示。长期方案是在训练数据里加入遮挡样本用随机矩形遮挡或粘贴模拟物生成增强数据。这类问题不要指望换个模型就能解决数据层面的补充才是根本。6. 用少量数据验证系统达标率一个练手的全套验证办法前面几章把检测模型和识别模型都搭好了但真正要交付一个车牌识别系统最后一步不能省——对着一个带标注的测试集做端到端评估量化“系统准确率”而不是单模型指标。这个评估结果就是你和别人汇报的核心数据。具体做法是准备一个测试视频人工逐帧标注车牌的 GT 文本比如京A12345写一个脚本跑完整条推理管线把每帧的输出与 GT 对比。评估指标用三个检测召回率GT 车牌中被检测框命中的比例、字符准确率检测框内字符全部正确的比例、整体识别率检测和识别都正确的端到端比例。如果整体识别率低于 80%按第 4.3 节的定位方法拆分先单独跑检测模块把漏检的帧挑出来再单独跑识别模块把识别错的裁剪图批量导出肉眼过一遍——这两种图特征完全不同很快能找到瓶颈模块。一个我亲测有用的验证技巧是把同一辆车从远到近驶过的视频段取关键帧观察整体识别率随车牌像素宽度检测框宽的变化曲线。通常车牌在画面里的宽度小于 60 像素时识别率急剧下降。这个数据直接决定摄像头安装高度和补光方案如果车牌的像素宽度长期小于 80识别率天花板就摆在那里再怎么调模型也白搭。碰到这种场景我会直接建议把摄像头架低一点或者换长焦镜头而不是继续优化模型。最后关于部署到 RK3588 这类边缘设备的优化思路检测模型用 ultralytics 官方导出 ONNX 再转 RKNNINT8 量化后推理时间能降到 20ms 以内LPRNet 导出 ONNX 后先检查 LSTM 算子能否被 RKNN 编译器支持——如果不支持把 LSTM 替换成宽卷积加全连接处理重新训练几轮识别率损失不大但部署省心得多。每次换部署平台都要重新做一遍第 4.3 节的耗时拆分不要想当然照搬参数。这套方案我做过不止一遍最大的教训是不要把精力全花在刷模型指标上——数据质量、模块边界和后处理细节对最终系统表现的影响往往比换一个更强的骨干网络来得大。先跑通最小闭环再用验证办法定位瓶颈按数据说话这套流程能帮你避开车牌识别系统里绝大多数隐藏的坑。希望帮到你。本文还有配套的精品资源点击获取