
简介目标检测与OCR文字识别是计算机视觉领域的两大核心任务二者的技术组合在智能交通和安防场景中有着广泛应用。车牌识别作为典型的工程落地案例通常采用“检测识别”的两阶段架构先通过目标检测模型从复杂场景中精准定位车牌区域再利用轻量级识别网络完成字符序列解码。YOLOv5凭借成熟的生态和高效的部署能力成为车牌定位的优选方案LPRNet则通过CNNRNN与CTC损失函数实现不定长车牌字符的端到端识别。本文从环境搭建、CCPD数据集处理、模型训练调优到串联推理完整拆解一套可复用的车牌识别系统实现路径并针对实际部署中的常见问题给出排查建议适合希望快速落地CV项目的开发者参考。1. 项目整体思路与方案选型1.1 为什么是“检测识别”两阶段结构很多刚接触车牌识别的朋友第一反应是找一个能直接输出车牌号的模型一步到位。但真实场景里车牌识别从来不是单一模型能搞定的事。你仔细想一下一张图中车牌可能只占很小一块区域背景里有树、有行人、有车灯反光、有广告牌文字如果让识别模型直接在整个图上找字符模型不仅要学“字符长什么样”还要学“车牌长什么样”“车牌可能出现在哪里”任务耦合太深训练难度和误识别率都会明显上升。所以主流做法是把任务拆成两步第一步用目标检测模型把车牌区域从整图中框出来第二步用识别模型对裁剪后的车牌小图做文字识别。这个“检测识别”的两阶段结构本质上是在模仿人看车牌的过程——你先找到车牌在哪再仔细读上面的字。两个模型各司其职检测模型专注定位识别模型专注字符训练起来都更轻松效果也更可控。我选择YOLOv5做检测、LPRNet做识别并不是因为它们是最新的模型恰恰是因为它们足够成熟、文档齐全、踩坑经验丰富。在工程项目里“稳”比“新”重要得多。YOLOv5在目标检测领域已经被验证了无数次而LPRNet是一种轻量级车牌识别网络专门为车牌这种定长或不定长的字符序列设计配合CTC损失函数不需要逐字符精确标注训练成本低、推理速度快非常适合工程落地。1.2 模型选型对比与理由先说我为什么没有用端到端方案。像LPRNet本身也可以接一个检测头或者用一些多任务网络同时输出位置和字符但这类方案对数据标注要求极高训练起来也不容易收敛。端到端方案看起来简洁实际操作中一旦检测和识别任意一个环节出问题你很难定位是网络的哪一部分出了问题。两阶段方案虽然部署时多一道工序但每一阶段都可以单独优化、单独验证排障方便得多。再分别说一下两个模型的选型理由。检测端我选YOLOv5而不是YOLOv8或RT-DETR原因很实在第一YOLOv5的生态太完善了从数据标注、训练到导出ONNX、TensorRT每一步都有大量现成资料遇到问题基本都能搜到解决方案第二YOLOv5对硬件要求不苛刻GTX 1660这种老显卡也能跑起来对个人开发者很友好第三它的s/m/l模型系列覆盖了从边缘设备到服务器的部署场景后续做工程化时选择空间大。识别端选LPRNet核心原因是它在速度和精度之间取得了很好的平衡。LPRNet全称是License Plate Recognition Network它用CNN提取特征然后用RNN序列建模最后接CTC损失进行解码。这个结构不需要预先知道车牌字符的精确数量哪怕车牌字符位置有偏移也能正确识别而且模型本身很小只有几MBCPU上跑一次推理也就几毫秒。相比之下如果直接用CRNN或者Attention-based OCR模型虽然通用性更强但模型体积大、推理慢对车牌这种高度场景化的任务来说属于杀鸡用牛刀。方案优点缺点适用场景YOLOv5 LPRNet成熟稳定、速度快、易部署两阶段流程稍复杂工程落地首选边缘设备友好YOLOv8 CRNN检测性能更强识别通用性好模型重、部署门槛高服务器端、复杂场景端到端识别网络流程简单训练难、排障难研究探索不推荐工程使用1.3 整体处理流程整个项目的处理链路是这样输入一张图像先经过YOLOv5检测模型模型会输出若干个候选框每个框附带置信度分数和类别信息。在车牌检测场景里我们只关注类别为“plate”的框然后根据置信度过滤掉低质量检测结果再用非极大值抑制NMS去除重叠框。拿到检测框之后把框对应的图像区域裁剪出来做尺寸归一化送到LPRNet识别模型里。LPRNet输出一个字符概率序列通过CTC解码得到最终的字符串比如“京A12345”。这里有个细节CCPD数据集里的车牌都是中国蓝牌格式是“省份简称字母数字”的组合所以我还需要根据字符集对解码结果做一层合法性校验排除明显不合理的结果。整体流程看起来简单但每个环节都有不少坑。后文我会把环境搭建、数据准备、模型训练、串联推理一一拆开讲把我实际踩过的坑和验证过的参数配置都写出来方便你直接照着做。2. 环境准备与工具链搭建2.1 软硬件基础环境说明先说硬件。YOLOv5训练对GPU有硬性需求我这边用的是一块RTX 3060 12G显卡训练YOLOv5s这个尺寸的模型绰绰有余。如果你只有CPU也不是不能跑但训练时间会非常痛苦不建议尝试。推理阶段CPU也能跑YOLOv5s在CPU上单张图大约需要100~300毫秒LPRNet在CPU上只要几毫秒整体还是可以接受的。软件环境我用的是Ubuntu 20.04 Python 3.8 CUDA 11.3 PyTorch 1.12。这里提个建议不要盲目追求最新版本。PyTorch 2.x当然更快但YOLOv5官方仓库很多第三方扩展脚本还在用旧接口版本太新反而容易出兼容性问题。我用这套组合跑了很久稳定性很好。提示CUDA、cuDNN、PyTorch这三者的版本必须严格匹配。装之前先去PyTorch官网用在线配置工具生成对应的安装命令别自己乱猜版本号。2.2 YOLOv5环境搭建步骤YOLOv5的环境搭建其实很简单但前提是网络能正常访问GitHub和PyPI。如果拉取代码或安装依赖经常超时可以把pip源换成国内镜像这个操作很常规。# 1. 克隆YOLOv5官方仓库 git clone https://github.com/ultralytics/yolov5.git cd yolov5 # 2. 创建虚拟环境强烈建议别把依赖装在全局环境里 python3 -m venv venv source venv/bin/activate # 3. 安装PyTorch根据CUDA版本选择对应命令 pip3 install torch1.12.0 torchvision0.13.0 --extra-index-url https://download.pytorch.org/whl/cu113 # 4. 安装YOLOv5依赖 pip install -r requirements.txt # 5. 验证环境 python detect.py --source data/images/bus.jpg如果最后一步能正常输出检测结果说明环境没问题。这里有个小技巧先跑一次官方的图片检测能区分“环境没装好”和“后续你自己的数据/配置有问题”这两种情况排障范围一下子缩小一半。2.3 LPRNet相关依赖LPRNet的依赖比YOLOv5简单得多核心就三个PyTorch、OpenCV、numpy。如果你已经跑通了YOLOv5LPRNet的环境基本不用额外装什么。# 在同一个虚拟环境里确认以下包已安装 pip install opencv-python numpyLPRNet没有像YOLOv5那样有官方一体化仓库代码通常是自己组织或者参考开源实现。我建议你自己维护一个工程目录把检测和识别的代码分开后续串联时也方便管理。目录结构可以参考这样plate_project/ ├── detection/ # YOLOv5相关代码 ├── recognition/ # LPRNet相关代码 ├── datasets/ # CCPD数据集 │ ├── images/ │ └── labels/ ├── weights/ # 存放训练好的模型权重 ├── scripts/ # 辅助脚本 └── test_images/ # 测试图片3. CCPD数据集理解与预处理3.1 数据集结构解读CCPDChinese City Parking Dataset是中科大收集的中国城市停车场景车牌数据集全部是蓝底白字的中国标准车牌图像来自真实停车场监控视角图片尺寸统一为1160×720。整个数据集按省份划分其中CCPD2019和CCPD2020是不同年份的版本CCPD2020增加了雾天、雨天等恶劣天气的数据。我用的CCPD2019数据集压缩包大约7GB包含超过25万张图片。每张图片的文件名里其实编码了大量标注信息这也是CCPD一个比较特别的地方。文件名格式是这样的皖A-12345-99-84-20-30-52-250-20这个文件名拆开来包含车牌号、车牌颜色、车牌的四个角点在图中的坐标等信息。所以严格来说CCPD自带标注不需要额外标注工具。但你如果直接用YOLOv5训练需要把文件名里编码的信息转换成YOLO格式的txt标注文件这一步需要写脚本解析文件名。文件名中关键信息位的含义大致如下第一个字段是车牌号包含省份简称和号牌后面跟着的是车牌四个顶点坐标以及一些附加信息。具体每个字段的精确含义官方README里有说明但比较简略建议直接看社区解析文章或者用几行代码把文件名解析出来对照实际图片验证一下。3.2 数据集的筛选与划分CCPD虽然数据量大但直接全量训练有几个问题。第一数据量太大训练一轮要很久实际收敛并不需要这么多第二CCPD里有些图片车牌很小、模糊甚至被遮挡这类图片作为训练数据的价值有限第三原始数据集没有划分训练集和验证集需要自己处理。我在实际项目中从CCPD2019里随机抽取了4万张图片作为训练集5000张作为验证集。4万张对车牌检测这种单一类别的任务来说已经非常充裕了。你可能觉得25万张不用很浪费但你要想清楚数据量大意味着迭代慢调试周期长先用子集把整个流程跑通再逐步加数据才是高效的做法。注意不要随便从网上找个已经划分好的CCPD子集。有些子集是别人挑剩下的难例分布和你自己随机抽的完全不同容易给你的模型带来bias。3.3 标注文件转换YOLOv5需要的标注格式是每个图片对应一个同名txt文件每行表示一个目标格式为class_id x_center y_center width height其中坐标都是归一化到0~1的相对值。CCPD的文件名里编码了车牌四个角点的坐标我需要先计算车牌的边界框然后做归一化。这一步看起来简单但有个细节要注意CCPD坐标系原点在图片左上角x轴向右y轴向下计算边界框时直接用最小外接矩形即可。# filename: 如 皖A-12345-99-84-20-30-52-250-20 import re def parse_ccpd_filename(filename): parts filename.split(-) plate parts[0] # 去除省份简称后的车牌号注意部分省份简称占两个字符 # 实际解析时需要根据CCPD官方文档确定每个字段 # 下面是以常见解析逻辑为例 left float(parts[3]) top float(parts[4]) right float(parts[5]) bottom float(parts[6]) return plate, left, top, right, bottom def convert_to_yolo(img_width, img_height, left, top, right, bottom): x_center (left right) / 2 / img_width y_center (top bottom) / 2 / img_height width (right - left) / img_width height (bottom - top) / img_height return f0 {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}注意不同CCPD版本的字段顺序可能有差异你要先打印解析结果用OpenCV画框对照原图确认坐标正确后再批量生成标注文件。这一步非常重要标注错了后面的训练等于白做。4. 车牌检测模型训练YOLOv5实操4.1 数据集配置文件YOLOv5通过YAML文件来指定数据集路径和类别信息。我创建了一个ccpd.yaml内容如下train: /path/to/plate_project/datasets/train/images val: /path/to/plate_project/datasets/val/images nc: 1 names: [plate]这里nc表示类别数我们只检测车牌一类所以是1。类别名叫plate即可不需要更复杂。4.2 模型结构选择与超参数调整YOLOv5提供n/s/m/l/x几个不同规格的模型从小到大。考虑到车牌检测任务相对简单而且后续可能要部署到边缘设备我选择了YOLOv5s作为基础模型。直接用预训练权重做迁移学习是必须的。YOLOv5官方在COCO数据集上训练好的权重学到了通用特征提取能力迁移到车牌检测任务上可以大大缩短训练时间。加载预训练权重的方式很简单训练命令里加上--weights yolov5s.pt即可。超参数我用的是YOLOv5自带的data/hyps/hyp.scratch-low.yaml这是官方推荐的低资源超参数组合适合显存不大、数据量中等的场景。我实际训练时的关键参数如下python train.py \ --data ccpd.yaml \ --weights yolov5s.pt \ --img 640 \ --batch-size 32 \ --epochs 80 \ --device 0 \ --hyp data/hyps/hyp.scratch-low.yaml几个参数的考量解释一下--img 640输入分辨率。CCPD原始图片是1160×720降到640可以显著降低显存占用和训练时间。车牌检测不需要极高分辨率640足够了。--batch-size 32在12G显存下YOLOv5s用640分辨率可以跑batch-size 32显存占用大概8~9G留出余量避免OOM。--epochs 80对于单类别检测80轮已经足够不需要像COCO那样跑300轮。4.3 训练过程监控与调优训练过程中我主要关注三个指标box_loss、obj_loss、cls_loss以及验证集上的mAP50和mAP50-95。在正常训练的情况下前10轮loss会快速下降mAP50从0快速升到0.9以上。如果mAP50一直上不去先别急着调参去检查标注文件有没有问题——我遇到过标注坐标解析错误导致模型怎么都学不会的情况排查了半天最后发现是坐标系搞反了。训练到30轮左右基本能收敛到mAP500.98以上mAP50-95大约0.75。这个精度对车牌检测完全够用了。如果你想把精度再往上推一点可以做以下几件事一是使用YOLOv5m模型容量更大精度会有小幅提升二是开启Mosaic数据增强和自动学习超参数也就是hyp.scratch-high.yaml三是用训练好的模型对训练集做一次伪标注把明显漏检的图片挑出来补充训练。经验车牌检测任务里mAP50-95不需要过分追求。因为后续车牌识别只关心检测框是否准确框住了车牌IoU在0.5以上通常就足够了mAP50才是更值得关注的指标。4.4 权重导出与验证训练完成后runs/train/exp/weights/下会有best.pt和last.pt两个权重文件best.pt是验证集表现最好的权重日常使用就选它。我习惯先做一次批量测试统计模型在验证集上的表现并可视化几个典型的检测结果看看是否存在漏检、误检、重复框等问题。python detect.py \ --weights runs/train/exp/weights/best.pt \ --source /path/to/val/images \ --conf-thres 0.3 \ --iou-thres 0.5 \ --save-txt \ --save-conf--save-txt会把检测结果保存为txt文件--save-conf会附带置信度方便后续做统计分析。在车牌场景里置信度阈值设置在0.3~0.5之间是比较合理的区间。设太高容易漏检模糊的车牌设太低会混入大量误检框增加识别阶段的无效计算。5. 车牌识别模型训练LPRNet实操5.1 LPRNet网络结构与原理LPRNet的核心结构可以概括为“CNN特征提取 序列建模 CTC解码”。它不需要先把车牌字符分割出来而是把整个车牌看作一个字符序列来处理。具体来说输入一张车牌图片经过若干卷积层提取特征得到一个特征序列然后这个序列经过RNN或者LSTM进行上下文建模输出每个时间步的字符概率分布最后通过CTC解码找出概率最大的字符序列。CTC损失函数允许模型输出序列的长度与真实标签长度不一致这样训练时不需要精确标注每个字符的位置非常灵活。这里有个对比传统OCR方案需要先做字符分割再逐个识别如果字符粘连或者模糊分割就很容易出错。LPRNet的序列建模方案天然避免了这个问题车牌字符之间的间距比较均匀但光照不均、反光、污损会导致字符边界不清晰用LPRNet这种端到端方式更鲁棒。5.2 字符集定义与编码中国车牌字符集包括省份简称、字母和数字。CCPD里只包含蓝牌字符集定义如下省份简称京、津、冀、晋、蒙、辽、吉、黑、沪、苏、浙、皖、闽、赣、鲁、豫、鄂、湘、粤、桂、琼、渝、川、贵、云、藏、陕、甘、青、宁、新共31个字母A-Z去除I和O因为和数字1、0太像24个数字0-9实际训练时我用的字符集是京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新ABCDEFGHJKLMNPQRSTUVWXYZ0123456789总共31241065个字符再加一个CTC空白符。训练时候有个问题格外重要LPRNet需要把图片resize到固定尺寸我使用宽94、高24这也是LPRNet论文中的输入尺寸。CCPD车牌裁剪出来的原始区域宽高比各不相同直接resize到94×24会拉伸变形但LPRNet对轻度变形不敏感实测影响不大。5.3 数据准备与增强从CCPD中按检测框裁剪出车牌区域后我做了两类数据增强第一类是模拟真实场景的光照变化。车牌识别最容易翻车的场景是反光和暗光所以我用OpenCV随机调整亮度、对比度并随机添加高斯噪声增强模型对光照变化的鲁棒性。第二类是几何扰动。车牌在检出的图像里不一定是完全水平的可能有一定旋转。我会对裁剪出的车牌做小角度随机旋转±3度以内模拟检测框角度偏差。但要注意旋转角度不能太大否则车牌字符会明显变形反而让模型更难学。import cv2 import numpy as np def plate_augment(plate_img): # 随机亮度对比度调整 h, w plate_img.shape[:2] alpha np.random.uniform(0.8, 1.2) beta np.random.uniform(-20, 20) img cv2.convertScaleAbs(plate_img, alphaalpha, betabeta) # 随机小角度旋转 angle np.random.uniform(-3, 3) M cv2.getRotationMatrix2D((w/2, h/2), angle, 1) img cv2.warpAffine(img, M, (w, h), borderModecv2.BORDER_REPLICATE) # 随机高斯噪声 noise np.random.normal(0, np.random.uniform(0, 5), img.shape).astype(np.uint8) img cv2.add(img, noise) # 统一resize到LPRNet输入尺寸 img cv2.resize(img, (94, 24)) return img5.4 LPRNet训练过程LPRNet的训练参数我按以下配置batch_size 64 learning_rate 0.001 epochs 50 optimizer torch.optim.Adam(model.parameters(), lrlearning_rate) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size15, gamma0.1)训练时我用的损失函数是CTC LossPyTorch里对应torch.nn.CTCLoss。有几个参数需要特别注意blank0对应字符集索引为0的位置也就是CTC空白符。构建字符集时我把空白符放在索引0真实字符从索引1开始。zero_infinityTrue因为某些样本的CTC loss可能为无穷大设置这个参数可以避免梯度变成NaN。LPRNet训练很快我的数据集大约有5万张车牌图在RTX 3060上50轮大约只需要2小时。训练过程中我关注的核心指标是字符准确率和整牌准确率。整牌准确率指预测结果和真实车牌号完全一致的比例这是最终用户体验最直接的指标如果某个字符识别错了整个车牌就错了。最终训练下来整牌准确率在验证集大约能达到97%左右剩余3%的失败样本主要集中在模糊、反光、车牌倾斜过大的图片上。心得车牌识别任务中整牌准确率是第一优先级。提升整牌准确率的关键不在模型结构而在数据多样性。我后来增加了夜间、强反光、阴影遮挡的样本后整牌准确率从94%提升到了97%这说明数据比模型重要。6. 检测识别串联推理6.1 推理流程代码实现训练好两个模型后把它们串联起来做整体推理。核心代码如下import cv2 import torch import numpy as np class PlateDetectorRecognition: def __init__(self, det_weights, rec_weights, devicecuda): self.device device # 加载YOLOv5模型 self.det_model torch.hub.load(ultralytics/yolov5, custom, pathdet_weights, force_reloadTrue) self.det_model.conf 0.3 self.det_model.iou 0.5 # 加载LPRNet模型 self.rec_model LPRNet(charset_size66).to(device) self.rec_model.load_state_dict(torch.load(rec_weights, map_locationdevice)) self.rec_model.eval() def predict(self, img): # 1. 检测 detections self.det_model(img) plates [] for det in detections.xyxy[0].cpu().numpy(): x1, y1, x2, y2, conf, cls det if conf 0.3: continue # 2. 裁剪车牌区域 plate_img img[int(y1):int(y2), int(x1):int(x2)] if plate_img.size 0: continue # 3. 识别 plate_str self.recognize(plate_img) plates.append((plate_str, conf, [x1, y1, x2, y2])) return plates def recognize(self, plate_img): # 预处理 img cv2.resize(plate_img, (94, 24)) img cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) img img / 255.0 img torch.FloatTensor(img).unsqueeze(0).unsqueeze(0).to(self.device) # 推理 with torch.no_grad(): logits self.rec_model(img) # CTC解码 preds logits.argmax(dim2).squeeze(1).cpu().numpy() result ctc_decode(preds) return result6.2 CTC解码细节CTC解码是识别阶段最关键的后处理步骤本质上是把模型输出的概率序列转换成最终的字符序列。解码逻辑主要分两步首先对每个时间步取概率最大的索引得到一个原始索引序列。然后合并连续重复的索引再去掉空白符。代码实现如下def ctc_decode(preds): # preds: 形状为(L,)是每个时间步最可能的字符索引 charset 京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新ABCDEFGHJKLMNPQRSTUVWXYZ0123456789 prev -1 result [] for idx in preds: if idx ! prev and idx ! 0: # 0是blank result.append(charset[idx - 1]) prev idx return .join(result)这一步有个隐含问题CTC的解码结果是最大概率路径的近似不一定是最优路径。理论上应该用beam search解码但在实践里车牌字符个数本来就不多7位贪心解码的精度已经足够beam search带来的提升微乎其微还拖慢速度。所以我直接用贪心解码省事且够用。6.3 性能优化与工程化建议串联推理时我实测下来YOLOv5s单张图在GPU上大约5~8毫秒LPRNet大约1~3毫秒整体20毫秒以内。这个速度足够应对实时视频流的检测任务。如果还想继续压榨性能有几个思路第一把YOLOv5和LPRNet都导出为TensorRT引擎在GPU上能再提速30%~50%。不过TensorRT的算子兼容性偶尔会出问题需要花时间调试。第二对YOLOv5做模型剪枝。对于车牌这种单一类别检测任务YOLOv5s里很多通道可能都是冗余的。用torch_pruning或者YOLOv5自带的剪枝脚本可以剪掉40%~50%的通道精度几乎不掉速度明显提升。但剪枝需要重新训练微调工程量不小。第三在CPU部署场景下可以考虑用OpenVINO加速。YOLOv5官方支持导出OpenVINO格式LPRNet也可以转整体推理速度在CPU上能提升2~3倍。6.4 实际效果与失败案例分析我在真实的停车场监控视频截帧上做了测试白天光线好的情况下检测识别几乎不犯错整牌准确率超过98%。夜间或者逆光场景准确率会下降主要问题出在检测端车牌区域过暗YOLOv5偶尔会漏检。这种情况下先对图像做一次自适应直方图均衡化CLAHE可以明显改善检测率。另一个常见问题是新能源车牌。CCPD数据集只包含蓝牌如果测试数据里有绿牌新能源车识别模型完全不认识。如果业务场景需要支持绿牌必须提前准备带新能源车牌的样本否则模型会硬把绿牌识别成蓝牌格式输出一堆乱码。好在新能源车牌标准字符集和蓝牌基本一致只需要补充数据训练即可。7. 常见问题与排查技巧7.1 问题速查表问题现象可能原因排查与解决办法YOLOv5训练loss不降标注文件坐标错误用OpenCV画框可视化标注确认解析正确YOLOv5检测漏检严重置信度阈值过高调低conf-thres观察检测结果检测框偏移、框不完整标注转换时坐标归一化错误检查图片尺寸是否用错ccpd.yaml的路径是否正确LPRNet训练loss为NaN学习率太大或batch内出现空序列降低学习率检查数据预处理是否有空图识别结果多一位字符CTC解码逻辑错误检查索引与字符集的对应偏移是否一致GPU显存不足batch_size太大或分辨率太高减小batch_size开启梯度累积训练和推理结果差异大数据预处理不一致确认训练和推理时resize、归一化方式完全一致检测正常但识别全是乱码字符集定义和模型输出索引不一致打印模型输出索引分布对照字符集检查7.2 两个最容易踩的坑这个项目的坑不少但有两个最容易让新手卡住好几天的地方我单独拎出来说。第一个坑是LPRNet的输入通道问题。LPRNet原始论文里输入是三通道RGB图但有些复现代码会把输入改成单通道灰度图。如果你下载了一个开源的LPRNet实现它内部第一层卷积的输入通道数可能和你喂进去的图像通道数不匹配报错还是小事最怕的是模型能跑但结果全错。我建议统一用三通道输入预处理时把灰度图转回三通道兼容性最好。第二个坑是YOLOv5检测到多个重叠框。车牌区域在图像里很显眼YOLOv5有时会同时输出多个高度重叠的框NMS虽然能过滤大部分但如果NMS的IoU阈值设置太高还是可能留下重复框。重复框会导致同一个车牌被识别多次增加无效计算。解决方案是把NMS的IoU阈值从默认的0.45降低到0.3实测能有效减少重复检测。注意在后处理环节我还加了一个小逻辑如果检测到的多个框中心点距离很近且宽度、高度接近就认为是同一个车牌只保留置信度最高的那个。这个后处理逻辑不消耗额外算力但能明显提升输出结果的稳定性。7.3 模型退化问题很多人在训练完成、效果不错之后就直接部署上线了过一阵子发现模型效果越来越差还以为是模型自己退化了。实际上不是模型退化了而是测试数据的分布变了。比如说你的摄像头从白天切换到夜间模式图像整体变暗或者停车场新增了某种新能源车牌或者刮风下雨摄像头上有水渍。这些情况都会导致模型输入分布和训练分布不一致效果自然下降。解决方案是建立持续的数据反馈闭环定期从实际场景中收集一批新图片人工标注后并入训练集重新训练。对车牌识别这种场景化很强的任务数据维护比模型调参重要得多。我个人的经验是每季度用新增的数据做一次增量训练模型效果会越来越稳。8. 个人实操体会与后续扩展建议8.1 项目整体感受做完这个项目我自己最深的体会是车牌检测识别这个经典场景技术栈已经非常成熟真正决定项目成败的不是模型选型而是数据处理和工程细节。YOLOv5和LPRNet虽然都不算最新技术但组合起来的效果非常能打。CCPD数据集质量高、规模大配合这两个模型做出一个能直接落地的车牌识别系统工作量并不大。如果你正打算做类似的项目我的建议是不要纠结于“有没有更好的模型”先把这条链路完整跑通有富余时间再去想优化的问题。还有一个体会是版本兼容性会消耗你大量精力。YOLOv5的仓库一直在更新不同版本的hyp文件、train.py的参数名都有差异如果你在网上搜教程最好认准同一个版本的代码混用不同版本的命令很容易踩到参数不匹配的坑。8.2 可扩展的方向这个项目后续能做的东西其实很多简单列一下我自己尝试过或者觉得值得尝试的方向第一支持新能源车牌。在现有蓝牌基础上扩充绿牌训练数据即可检测端和识别端的模型结构都不用变只需微调类别和数据。第二部署到边缘设备。YOLOv5和LPRNet都很轻量移植到Jetson Nano、RK3588这类边缘设备完全可行。导出成TensorRT或OpenVINO格式后可以在嵌入式环境里跑实时识别。第三多角度、多国家车牌支持。如果想做海外车牌识别需要重新标注数据LPRNet的字符集也需要相应扩展。训练框架本身可以复用。第四与车辆检测、车型识别等任务结合构建完整的车辆结构化分析系统。车牌只是一个维度如果把车身颜色、车型、品牌都识别出来就是一套更完整的车辆属性分析方案。8.3 最后一个实用建议最后分享一个小技巧在做车牌识别项目时不要只盯着准确率指标。实际部署中用户更关心的是“识别不出来的时候系统是怎么表现的”。我建议在识别置信度低的时候不要硬输出一个可能错误的结果而是返回“无法识别”状态让系统走人工复核流程。这个简单的设计能把用户体验提升一个档次。车牌识别这个领域技术门槛已经不高了真正比拼的是对业务场景的理解和对细节的把控。希望这篇实战记录能帮你少走一些弯路。本文还有配套的精品资源点击获取