YOLOv5+LPRNet车牌识别实战:从CCPD数据预处理到模型串联部署

📅 发布时间:2026/8/27 1:33:51
YOLOv5+LPRNet车牌识别实战:从CCPD数据预处理到模型串联部署 简介目标检测与光学字符识别OCR是计算机视觉工程落地中最常见的两类任务。车牌识别作为二者的典型结合场景在智能交通、无人停车场等系统中扮演关键角色。当前业界普遍采用“检测识别”两级模型架构先由目标检测算法定位车牌区域再由轻量级识别网络完成字符序列解码。YOLOv5具备高精度、易部署的特性常用于车牌框检测LPRNet则凭借免字符分割的序列建模能力配合CTC损失高效输出车牌字符串。CCPD数据集源自真实城市停车监控场景包含大量倾斜、模糊、多光照样本为模型训练提供了可靠数据基础。文章围绕YOLOv5LPRNet技术路线完整梳理数据标注转换、模型训练、推理串联与工程调优要点为车牌识别项目的快速落地提供参考。 车牌识别这个方向我前前后后做了大半年从最初拿到一份网上打包好的“YOLOv5LPRNet车牌识别CCPD数据集”压缩包到本地调通、复现再到把两个模型都重新训练、串联部署踩了不少坑。这篇文章就基于这套技术路线把数据准备、模型训练、推理联调的整个流程完整复盘一遍CCPD数据集的整理方法、关键参数和字符集设计也会直接摊开讲。新手可以照着一步步跑通有基础的朋友可以直接看数据预处理、模型串联和字符集这些容易翻车的地方。我先说结论YOLOv5负责“框出车牌在哪里”LPRNet负责“读出车牌是什么”。两个模型各管一段训练和调优都更灵活比直接端到端识别整图的方案好用很多。下面按完整实操顺序来说把我实际跑过的命令和代码都给你。1. 项目整体设计与思路拆解为什么是YOLOv5 LPRNet1.1 两级架构与端到端方案的取舍车牌识别通常有两种做法一种是用一个目标检测网络直接输出车牌号端到端像百度PaddleOCR里的一些多任务模型另一种是先检测车牌区域再对裁剪下来的车牌图像做字符识别。我这次选的是第二种原因很实际车牌在整张监控图里通常只占很小一块端到端模型想同时学“定位”和“识别”对数据量和算力的要求更高而且一旦某个停车场的光照、拍摄角度变化很大模型容易整体崩掉。两级方案里YOLOv5负责检测车牌目标LPRNet只负责识别已经裁剪好的车牌图像。这样做的好处非常明显检测模型换到新的监控场景时只需要重新标注车牌框、重训检测头识别模型面对不同省份、不同样式的车牌也不需要动定位逻辑。两个模块分开训练、分开验证哪个环节出了问题可以直接定位不会出现“输出错了但不知道是检测错了还是识别错了”的情况。1.2 CCPD数据集的特点与利用价值CCPDChinese City Parking Dataset是国内停车场监控场景下采集的开源车牌数据集总规模接近30万张里面的图像来自不同城市、不同光照、不同角度包含大量倾斜、模糊、遮挡的车牌。这个数据集最大的价值是“真实”不是实验室里摆拍的干净图片而是摄像头直出画面所以训练出来的模型拿到实际场景里也能用。但CCPD的坑也很明显。它的标注文件不是YOLO那样的矩形框而是JSON数组里的四个顶点坐标用来表示车牌的四边形位置。而且车牌本身经常是倾斜的如果直接按四个顶点的最小外接矩形切出来会带进不少背景区域如果直接丢给识别模型又会引入很多干扰。另外标签文件里还包含车牌号字符串里面有省份汉字、字母和数字处理字符集的时候要格外小心。我建议不要一开始就把30万张全量训一遍时间成本和显存压力都很大。先抽个5000到10000张覆盖不同的天气、倾斜角度和车牌样式把流程跑通再决定要不要加数据。CCPD数据集的原始图像分辨率不低通常在一千像素级别直接拿来做YOLOv5训练时如果显存不够就先用子集。流程图这种事我不画了但整个链路就是“CCPD原始图 - JSON标注转YOLO框 - YOLOv5检测车牌 - 按框裁剪 - 透视矫正 - LPRNet识别字符 - 输出字符串”。2. 环境搭建与数据准备把CCPD标注转成YOLO能吃的格式2.1 依赖安装与目录结构规划YOLOv5我建议直接用官方仓库方便后续更新。安装步骤并不复杂拉代码、建虚拟环境、装依赖。Python版本推荐3.8以上PyTorch建议1.8以上GPU优先。如果你的机器没有NVIDIA显卡也能用CPU跑通流程但训练时间会长很多推理一帧可能要到几百毫秒部署阶段再考虑用GPU或转ONNX优化。LPRNet这边通常是一个单独的PyTorch实现依赖OpenCV、NumPy、Pillow这些基础库。从压缩包解压后建议把项目目录整理成这样plate_recognition/ ├── yolov5/ ├── lprnet/ ├── data/ │ ├── CCPD/ │ │ └── ccpd_2019/ │ └── datasets/ # 存放转换后的训练数据 ├── preprocess/ └── inference/这样YOLOv5和LPRNet的代码不互相污染后续换模型版本也方便。第一次跑通时不需要建太复杂的目录能分清就行。另外建议把CCPD原始图片单独放一个文件夹转换完的标注和图片再放到另一个目录不然原始数据和训练数据混在一起很容易在做数据增强时出错。2.2 车牌标注解析与转换脚本CCPD的JSON标注结构比较典型每个图像对应一个同名.json文件里面objects数组保存目标信息vertices就是车牌的四个角点坐标顺序一般从左上角开始顺时针。我需要把这些坐标转成YOLOv5需要的格式每行是class cx cy w h其中中心点坐标和宽高都归一化到0到1之间。下面是我实际用过的转换脚本做了三件比较重要的事一是从四个顶点里取最小外接矩形的四个角二是过滤掉面积太小的目标三是把标签里可能出现的空白字符清掉。import json import os import cv2 import numpy as np def ccpd_to_yolo(json_path, img_width, img_height, output_txt_path): with open(json_path, r, encodingutf-8) as f: data json.load(f) lines [] for obj in data[objects]: if obj.get(label) is None or obj[label] : continue vertices obj[vertices] # 四个点 [(x1,y1), (x2,y2), ...] pts np.array(vertices, dtypenp.float32) xmin pts[:, 0].min() xmax pts[:, 0].max() ymin pts[:, 1].min() ymax pts[:, 1].max() w xmax - xmin h ymax - ymin if w 10 or h 10: # 过滤过小目标 continue cx (xmin xmax) / 2.0 / img_width cy (ymin ymax) / 2.0 / img_height w_norm w / img_width h_norm h / img_height # 处理宽高越界 cx min(max(cx, 0), 1.0) cy min(max(cy, 0), 1.0) w_norm min(w_norm, 1.0) h_norm min(h_norm, 1.0) lines.append(f0 {cx:.6f} {cy:.6f} {w_norm:.6f} {h_norm:.6f}) with open(output_txt_path, w) as f: f.write(\n.join(lines))这段脚本看起来简单但有几个细节要注意。CCPD图像里也可能存在其他车辆或背景目标但标注本身只关心车牌所以class固定为0。如果后续你想同时检测“大车牌”和“新能源车牌”两种类型可以把class拆成0和1但识别侧后期还是需要处理8位字符检测端分开意义不大我建议先统一检测车牌框字符数交给LPRNet那个环节来处理。2.3 数据划分与清洗的实操经验转换完标注后要把图片和txt按训练集、验证集、测试集分开。我习惯按“图片所属的原始子目录”来划分而不是直接全局随机。因为CCPD里同一个停车场、同一个监控摄像头拍摄的连续帧非常相似全局随机会把几乎一样的图片同时分进训练集和验证集导致验证指标虚高真到新场景就掉点。划分时我还会做一次基础清洗过滤掉车牌字符长度异常比如普通车牌应该是7位新能源是8位少于5位或大于9位的基本都有问题和顶点坐标明显不合理的样本。另外CCPD里的车牌图像可能非常倾斜如果你打算用YOLOv5检测水平框我建议不要硬训那种倾角超过45度的极端样本先保证大多数正常样本学好再慢慢加入困难样本。最后创建dataset.yaml给YOLOv5训练用train: data/datasets/images/train val: data/datasets/images/val nc: 1 names: [plate]这里的路径要写相对YOLOv5项目根目录的路径或者填绝对路径我因为经常挪项目目录填绝对路径反而更省心但要注意部署时改路径。3. 车牌检测实战用YOLOv5训练自定义检测器3.1 模型配置与anchor调整我用的是YOLOv5s属于速度和精度比较平衡的中间档。检测车牌的难点不在类别多而在车牌属于小目标并且长宽比很夸张常见车牌宽度是高度的三倍左右所以默认的anchor不一定最优。YOLOv5训练时默认会重新做anchor聚类也就是所谓的autoanchor默认开启就行。如果你用了老版本或者想手动控制可以在训练命令里不加--noautoanchor让它自动算。如果发现检测框尺寸始终不贴合车牌比如框太大或太小可以先检查一下聚类出来的anchor尺寸。车牌在640分辨率下通常只有40到80像素宽20到40像素高如果你看到anchor里全是100像素以上的大框很可能是训练图像里车牌占比偏小建议把输入分辨率从640提高到960或者做切片推理。不过提高分辨率会明显增加显存占用我的做法是先用640跑通流程再在需要精度的场景单独用960微调。另一个容易踩的坑是数据增强中的翻转。YOLOv5默认会做随机水平翻转和垂直翻转对物体检测来说没什么问题但车牌是文字垂直翻转会让检测器看到大量“倒立”车牌框虽然框的位置其实没变但它会让模型把“车牌区域”和“文字方向”绑定得很奇怪。我实际测试下来检测端开着翻转影响不大因为只学“位置”不学“字符内容”但如果你后续还要做车牌角度分类就需要保存翻转标记麻烦得很。所以我在训练时直接关掉了垂直翻转水平翻转保留因为车牌左右镜像后仍然是车牌检测任务不受影响。3.2 训练命令与超参数选择训练命令大概是这样的cd yolov5 python train.py \ --data ../data/datasets/plate.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --device 0 \ --cache ram这里--cache ram是把图片预加载到内存训练会快很多但要求内存至少16G以上。如果内存不够就删掉这个参数。--weights可以先下载官方yolov5s.pt预训练权重迁移学习比从零开始训练收敛快很多。如果数据量少--epochs可以加到150但要注意别过拟合。训练过程中我主要盯两个指标mAP_0.5和mAP_0.5:0.95。前者判断检测框大致准确率后者更严格。车牌检测任务通常mAP_0.5能到0.98以上但mAP_0.5:0.95可能只有0.85到0.9这很正常因为车牌框比较小检测框稍微偏移一点IoU就会掉很多。如果mAP_0.5:0.95偏低我一般会先提升输入分辨率再看anchor是否需要调整。我还试过用--hyp调整损失权重但效果不如直接加数据。有一招比较实用把训练图像中的车牌区域裁剪放大再贴回原图用类似“复制粘贴增强”的方式增加近处车牌的样本这在停车场出入口场景下效果很明显。YOLOv5的mosaic增强默认开着已经能缓解小目标问题。3.3 检测模块的验证与导出训练结束后用detect.py验证一下单张图效果python detect.py --weights runs/train/exp/weights/best.pt \ --source ../data/val_image.jpg --conf 0.4 --iou 0.5这里--conf建议设置在0.4到0.5之间。车牌检测的漏检和误检都很影响后续识别阈值低了误检多高了漏检多0.4算是一个比较折中的值。如果检测框总是被别的车辆部件干扰就把--iou调高一点比如0.6让NMS更严格减少重叠框。导出模型时我一般会转成ONNX方便跨平台部署python export.py --weights runs/train/exp/weights/best.pt --include onnx --opset 12导出为ONNX后可以用onnxruntime在CPU上推理速度比PyTorch默认模型快不少。要注意的是yolov5版本不同导出脚本的参数略有差异遇到问题先看官方文档。4. 车牌识别实战LPRNet训练与字符集设计4.1 LPRNet网络原理与CTC损失LPRNet是Mikhail等在2018年提出的一个轻量级车牌识别网络最大的特点是“免分割”。传统车牌识别要先切出每个字符的位置再去逐个分类一旦字符粘连、反光、模糊切割就很容易出错。LPRNet直接把整张车牌图送进去通过CNN提取特征再用RNN这里常用双向LSTM结构对特征序列建模最后用CTC损失把序列对齐到最终车牌字符串上。你可以把LPRNet理解成一个“看图说字”的模型它不关心每个字符精确的边界在哪里只关心整张图里出现过哪些字符以及它们的顺序。CTC允许模型输出比最终标签长很多的特征序列然后在解码时合并重复字符、跳过空白位从而得到最终的7位或8位车牌号。这种设计天然适合字符宽度不固定、字符之间有粘贴或形变的场景。我用的LPRNet实现里输入图像通常被resize成宽94、高24宽度不必保持原比例因为CTC对横向压缩并不敏感但高度必须固定否则CNN输出的特征图尺寸对不上。输入是灰度图通道数为1所以如果从彩色图裁剪出来要先转灰度。4.2 车牌图像预处理与字符集构建从CCPD标注里除了转换YOLO检测框还需要把四点坐标对应的车牌区域用透视变换矫正成水平矩形。这一步很关键因为CCPD里的车牌经常是斜的直接横着裁剪会让字符严重变形。矫正代码很简单def crop_and_warp_plate(full_img, vertices, dst_size(94, 24)): pts_src np.array(vertices, dtypenp.float32) # 根据四个角点排序得到左上右上右下左下 dst_pts np.array([[0, 0], [dst_size[0]-1, 0], [dst_size[0]-1, dst_size[1]-1], [0, dst_size[1]-1]], dtypenp.float32) M cv2.getPerspectiveTransform(pts_src, dst_pts) out cv2.warpPerspective(full_img, M, dst_size) return out注意原标注里的四个点不一定是从左上角开始的所以我实际用到这个函数前会先把四个顶点排序按y坐标和x坐标组合判断保证透视变换的源点顺序正确。否则矫正出来的图可能是倒的或者旋转90度的识别率会断崖式下跌。字符集设计我直接写成列表里面是“省份汉字 字母 数字”的组合。中国车牌不用O和I因为容易和0、1混淆所以在字符集里就要去掉这两个字母否则模型就算学到了O推理端还要做二次校正。我的字符集如下provinces 京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼 letters ABCDEFGHJKLMNPQRSTUVWXYZ digits 0123456789 chars provinces letters digits这里省份汉字只列了常见的31个实际CCPD里可能不完备但没关系后面训练时根据标签统计所有出现的字符再动态扩展字符集会更稳。字符集定义好之后每个字符映射到固定索引标签字符串转成索引序列作为LPRNet训练时的target。4.3 训练细节与精度调优LPRNet训练集的构建方式是从CCPD里按照标注裁剪出车牌图然后保存成一张张24x94的单通道图同时生成一个标签文件每行是图片路径和车牌字符串。我手动写了个批量裁剪脚本把验证集和测试集也一起裁出来。注意不要用YOLOv5训练时的同款图片做LPRNet测试不然识别精度会虚高。训练时我用的优化器是Adam初始学习率1e-3batch size设128如果你的显存小就降到64训练100个周期左右。损失函数用CTC Loss。PyTorch里的torch.nn.CTCLoss需要输入时间步长、目标长度这些参数我在LPRNet实现里已经把这些参数封装好了如果是从零写这一步最容易搞错。CTC的blank默认是类别数也就是说在字符集末尾加了一个空白类。训练过程中我每隔几个epoch就在验证集上跑一下计算字符级精确率和整串精确率。字符级精确率不要求整串完全正确只要每个字符位置对了都算整串精确率必须是7位或8位完全一致才算对。通常字符级能达到98%以上整串精确率在95%左右就算很好了。如果整串精确率上不去多半是卡在汉字或者下一位字母上CCPD里汉字样本分布不均有的省份车牌数量特别少需要对这些字符做重采样或数据增强。LPRNet的数据增强我建议控制在轻量范围亮度扰动、对比度扰动、细微的高斯噪声以及几度的旋转。不要做太强的透视变换因为那会让原本已经矫正好的车牌又变得歪歪扭扭反而不利于识别。另外不建议对车牌图像做强曝光或者直方图均衡实际停车场的灯光复杂训练时引入太多增强会让模型对光照过度敏感。5. 检测识别串联从图片到最终车牌号的完整流程5.1 ROI裁剪与角度矫正的衔接检测模型输出的是水平矩形框但车牌本身可能带倾斜角。如果直接把水平框裁下来送进LPRNet识别率会受到很大影响。我在项目里做过对比不对倾斜车牌做矫正整串识别率会从95%掉到85%左右差距非常明显。所以串联时一定要加一个角度矫正步骤。最简单有效的矫正方式是拿到检测框后把框内的图像放大一点然后做边缘检测和直线检测找出车牌上下边缘的倾斜角再用仿射变换转正。这个方法对大多数情况有效但受光照影响较大反光严重时边缘检测会乱。另一个办法是训练一个轻量的角度分类模型把角度离散成几个档位比如-15度、0度、15度然后按类别矫正。如果你不想额外训练模型就用“多角度旋转识别”把裁剪图分别旋转-10、0、10度每个角度都送进LPRNet选择置信度最高的结果效果也不错代价是推理时间变成三倍。我的代码里采用了这样一个流程def detect_and_recognize(image, detector, recognizer, angle_candidates(-10, 0, 10)): dets detector(image) # 返回boxes, scores, class_ids results [] for box, score in zip(dets[0].boxes.xyxy.cpu().numpy(), dets[0].boxes.conf.cpu().numpy()): if score 0.4: continue x1, y1, x2, y2 [int(v) for v in box] crop image[y1:y2, x1:x2] best_text, best_conf , 0 for angle in angle_candidates: rotated rotate_crop(crop, angle) # 按角度旋转并保持尺寸 text, conf recognizer.predict(rotated) if conf best_conf: best_text, best_conf text, conf results.append((best_text, float(score * best_conf))) return results这个多角度投票策略在普通停车场场景下基本够用而且不用额外标注角度数据是个很划算的实战技巧。5.2 识别后处理与结果过滤LPRNet识别出来的原始字符串可能包含无效字符、重复字符、空白位所以必须做后处理。解码阶段我采用贪心CTC解码对每个时间步取概率最大的类别索引然后按顺序合并连续重复的索引最后去掉空白索引。这个过程看起来简单但要注意一个细节LPRNet的字符序列里同一个字符可能在相邻帧重复出现比如“AA”这种连续相同的字符贪心解码会错误地把它们合并成一个“A”。要解决这个问题需要在编码时显式插入分隔符或采用beam search解码。我实际测试下来车牌字符里连续重复的概率不高所以先用贪心解码也能用但如果你要处理“京A·88888”这类含重复字符的样本最好加上分隔符逻辑。输出字符串后我会用正则做一个合法性校验import re normal_plate re.compile(r^[\u4e00-\u9fa5][A-Z][A-Z0-9]{5}$) new_energy_plate re.compile(r^[\u4e00-\u9fa5][A-Z][A-Z0-9]{6}$)普通燃油车牌是7位新能源车牌是8位中间没有分隔点。CCPD标签里的点号要去掉识别模型输出也不应该包含点号。如果模型输出了O或I这种不在字符集里的字母我会直接替换成0和1因为真实车牌不许用这两个字母替换后往往能修正不少错误。整串置信度我定义为所有字符预测概率的平均值。如果某个字符概率特别低比如低于0.5我会把整串标记为低置信度在业务系统里可以再触发一次重识别或人工复核。5.3 推理性能与稳定性的优化如果视频流是25帧每秒单帧整个流程要控制在40毫秒内。用GPU时YOLOv5s大概5到10毫秒LPRNet大概1到2毫秒加上图像预处理可以满足需求。但如果用CPUYOLOv5s单帧可能就要100到300毫秒LPRNet也要几十毫秒这时候就得靠工程手段补比如用ONNX Runtime或OpenVINO加速或者降低到每N帧检测一次然后用跟踪器在中间帧估计车牌位置。还有一个稳定性的坑是“检测到多个车牌”。停车场出入口经常同时出现多个车牌但业务系统往往只需要识别最靠近闸机的那一个。我一般会在后处理里优先选择检测框面积最大、且底边离图像底部最近的框因为视角近的车牌通常大且靠下。如果场景固定还可以直接在图像里划定一个感兴趣区域ROI只在该区域做检测既减少误检又提升速度。6. 常见问题与排查技巧实录6.1 检测阶段的高频雷区标签格式转换后训练不收敛先检查转换脚本里坐标到底归一化了没有。YOLOv5的txt文件里的坐标必须归一化否则训练过程loss会正常下降但推理框全跑偏到图像角落这是最常见的新手问题。mAP很高但小目标漏检CCPD里很多车牌在图中只有二三十像素宽YOLOv5在640分辨率下特征图最小步长已经很大小目标信息容易丢失。解决办法是输入分辨率调到960或者使用TTA测试时增强但TTA会拖慢推理速度只适合离线分析。框与车牌不完全贴合车牌倾斜导致水平框包含了较多背景。这个不用太纠结检测框只要能框住大部分车牌就能进入矫正环节。如果真的很难贴合可以试试YOLOv5-OBB旋转框方案但代价是要重新标注旋转框工程量大不少。6.2 识别阶段的高频错误省份汉字识别错误率高汉字字符类别多且部分省份在数据集里出现次数少。我建议在构建训练集时统计每个字符的样本量对低于某个阈值的字符做过采样或者从网上的开源车牌生成器补充一些合成数据。合成数据虽然和真实图有差距但对提高汉字识别率很有帮助。蓝牌和绿牌识别串场普通蓝牌和新能源绿牌的底色不同LPRNet如果训练时没有充分混合两类样本可能会在蓝牌上识别出绿牌字符长度。这个问题一般不是模型结构造成的而是数据分布不均。只要保证训练集中新能源车牌占比在15%以上基本就能缓解。图像resize导致字符被压缩变形LPRNet输入尺寸是94x24如果直接把宽高比接近3:1的车牌拉伸过去其实还好但如果你拿了一个特别宽的车牌去硬拉伸字符会变矮变宽。更稳妥的做法是先保持宽高比resize再在两侧填黑边到目标宽度。不过填黑边会增加一些背景干扰要先用分割或阈值处理把车牌区域抠出来。我实测下来在正常CCPD车牌上直接拉伸效果并不差所以这个坑不用太担心。6.3 全链路联调的建议检测和识别置信度要解耦检测阈值可以设在0.3到0.4之间识别阈值设在0.6到0.7之间。因为检测多几个候选框没关系识别模块会在后续过滤识别置信度低时宁可丢帧重试也不要给下游错误车牌。多帧结果要做时间平滑视频流里前一帧识别为“京A12345”后一帧识别成“京A12345”但少了一个字符这种跳变很常见。我会维护一个队列统计最近5帧的识别结果取出现次数最多的结果作为最终输出。如果是离线图片那就只能单帧结果直接出了。换到新场景时不要全量重训如果只是停车场入口换了相机角度先拿新场景几百张图片做测试看检测和识别分别错在哪。如果检测漏检多就补充检测标注如果检测没问题但识别错多半是角度和光照变化优先调整LPRNet的数据增强参数而不是上来就把两个模型都重训。最后一公里的经验这套YOLOv5LPRNet的组合我后来还移植到工业相机上做了实时抓拍系统。整个项目最大的收益是模块化带来的灵活性第一次训练完检测器后后来换到另一个停车场只需要重新标注几百张车牌框微调YOLOv5LPRNet几乎不用动因为字符识别任务的通用性比位置检测强得多。如果你手头的CCPD压缩包代码已经跑通建议下一步把LPRNet里的省份汉字单独抽出来做一层校验再针对新能源车牌做专门的模板匹配识别准确率还能往上拉一个点。最后再分享一个实操心得不要迷信大模型和复杂结构先把数据格式和字符集这两块地基打牢比换任何网络都管用。本文还有配套的精品资源点击获取