YOLOv5车牌识别实战:从数据标注到端侧部署全链路
简介这份资源面向深度学习初学者、目标检测实践者及毕业设计选题学生提供基于YOLOv5的完整车牌识别项目。压缩包共77个文件约25.4MB以29个Python脚本、17个YAML配置、21张示例图片为主另含2个预训练权重文件、6个Shell脚本及Dockerfile覆盖模型定义、训练、推理与部署全流程。项目包含YOLOv5各规格网络配置、数据转换脚本与车牌检测权重读者可据此完成数据预处理、模型训练、参数调优、验证测试到实时部署的完整链路理解卷积神经网络如何提取车牌特征并输出边界框。已有2591人学习下载适合作为课程实践、课题研究或智能交通场景入门参考帮助快速搭建可运行的车牌检测环境并掌握调参排错思路。1. 从一张模糊的卡口图说起YOLOv5 做车牌识别到底难在哪深夜卡口拍回来的图车牌区域往往只占整幅画面的百分之几还伴随运动模糊、车灯眩光、角度倾斜。很多人第一次用 YOLOv5 跑车牌识别训练 loss 降得很漂亮一上真实图就翻车要么框不住要么把「京」认成「津」。问题不在 YOLOv5 本身而在于车牌识别是一个「检测 识别」的复合任务而 YOLOv5 原生只解决检测这一半。这篇笔记讲的就是怎么用 YOLOv5 把车牌识别这条链路真正跑通从数据标注、环境配置、训练调参到检测框后处理、字符识别衔接再到部署时的性能取舍。适合已经会一点 Python、装过 PyTorch但还没把检测模型落到具体业务上的工程师也适合做过车牌识别但用的是传统 OpenCV 方案、想换成深度学习路线的朋友。整条链路我会按我自己落地的顺序讲参数给具体值坑给具体现象不绕弯子。2. YOLOv5 车牌检测的选型与数据准备为什么不用 SSD 和 Faster R-CNN2.1 车牌检测为什么 YOLOv5 比两阶段方案更合适车牌检测的本质是「小目标 高召回」。一张 1920×1080 的卡口图车牌框可能只有 120×40 像素占全图面积不到 0.3%。Faster R-CNN 这类两阶段检测器精度高但 RPN 生成候选框再逐个分类回归推理速度在同等硬件下通常是 YOLOv5s 的三到五倍慢而车牌场景往往要求单帧 30ms 以内出结果否则多路视频流根本扛不住。YOLOv5 是单阶段检测器一次前向就出框和类别工程上更好压榨。它有几个对车牌特别友好的设计一是 PANet 结构里的自底向上路径能把浅层高分辨率特征传到检测头小目标召回明显好于早期 YOLO二是 Mosaic 数据增强四张图拼一张等效于让模型在一张图里见到更多不同尺度的车牌对小目标训练很关键三是 anchor 可以按你自己的数据集聚类重设车牌这种宽高比接近 3:1 到 4:1 的目标用默认 COCO anchor 是浪费。选版本上我一般用 YOLOv5s 或 YOLOv5m。s 版本参数量约 7M在 1080Ti 上单帧 1080p 推理能到 10ms 出头m 版本精度高两三个点但慢一倍。车牌检测类别只有一类或者加个「双层车牌」两类不需要大模型容量s 足够。如果要做端侧部署比如树莓派 5 上跑自己训练的 YOLOv5 模型那更得用 s 甚至 nano还得配合 ONNX 或 NCNN 转换。2.2 数据标注车牌框怎么画才不坑自己数据是这条链路里最容易被低估的部分。我见过太多人拿几千张图随便标标就开训结果模型学到的其实是「车头位置」而不是「车牌位置」——因为标注时框画大了把保险杠也框进去了。标注工具用 labelImg 或 X-AnyLabeling 都行导出 YOLO 格式。关键规则有三条框必须紧贴车牌四边留白不超过 2 像素倾斜车牌用水平外接矩形不要用旋转框YOLOv5 原生不支持旋转框除非你换 OBB 分支模糊到人眼都认不出字符的图直接删别留着让模型学噪声。数据量上单场景比如固定卡口2000 到 5000 张就能出可用模型多场景不同城市、不同光照建议 1 万张起步。类别就一个license_plate。标注完检查一下每类框的数量分布如果某些光照条件下样本特别少后面训练时那类场景必然拉胯。2.3 用脚本把标注转成 YOLOv5 训练格式YOLOv5 要求的数据结构是 images 和 labels 分开labels 里每个 txt 对应一张图每行是class x_center y_center width height全部归一化到 0 到 1。下面这个脚本把 labelImg 导出的 VOC 格式转成 YOLO 格式并划分训练验证集。import os import random import xml.etree.ElementTree as ET from pathlib import Path # 输入VOC 格式的 xml 标注目录和图片目录 # 输出YOLOv5 要求的 images/labels 结构 train/val 划分 def voc_to_yolo(xml_dir, img_dir, out_dir, val_ratio0.1): xml_dir, img_dir, out_dir Path(xml_dir), Path(img_dir), Path(out_dir) for split in [train, val]: (out_dir / images / split).mkdir(parentsTrue, exist_okTrue) (out_dir / labels / split).mkdir(parentsTrue, exist_okTrue) xml_files list(xml_dir.glob(*.xml)) random.shuffle(xml_files) val_count int(len(xml_files) * val_ratio) for idx, xml_file in enumerate(xml_files): split val if idx val_count else train tree ET.parse(xml_file) root tree.getroot() size root.find(size) w, h int(size.find(width).text), int(size.find(height).text) lines [] for obj in root.iter(object): cls_name obj.find(name).text if cls_name ! license_plate: continue bbox obj.find(bndbox) x1 float(bbox.find(xmin).text) y1 float(bbox.find(ymin).text) x2 float(bbox.find(xmax).text) y2 float(bbox.find(ymax).text) # 归一化并转成中心点宽高格式 xc (x1 x2) / 2.0 / w yc (y1 y2) / 2.0 / h bw (x2 - x1) / w bh (y2 - y1) / h lines.append(f0 {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}) if not lines: continue img_name xml_file.stem .jpg src_img img_dir / img_name if not src_img.exists(): continue # 复制图片并写 label import shutil shutil.copy(src_img, out_dir / images / split / img_name) with open(out_dir / labels / split / (xml_file.stem .txt), w) as f: f.write(\n.join(lines)) if __name__ __main__: voc_to_yolo(./annotations, ./images, ./plate_dataset, val_ratio0.1)逻辑说明脚本先打乱所有 xml按比例切分 train/val然后逐个解析 xml 里的 bbox做归一化转换。参数val_ratio控制验证集比例单场景数据 0.1 够用多场景建议 0.15 到 0.2保证验证集覆盖各种光照。注意cls_name过滤那行如果你标了多种车牌类型这里要改成映射字典否则类别索引会错位。2.4 用 K-means 重设 anchor 让框更贴合车牌YOLOv5 默认 anchor 是 COCO 上聚类的宽高比偏方正车牌是扁长的直接用会导致正样本匹配质量差。跑一遍 K-means 聚类自己的标注框把结果填进模型配置。# YOLOv5 仓库自带聚类脚本先克隆官方仓库 git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt # 对车牌数据集做 anchor 聚类9 个 anchor输入尺寸 640 python utils/autanchor.py --data plate_dataset/data.yaml --img-size 640 --n 9跑完会输出类似[[10,13, 16,30, ...]]的数组把它替换到models/yolov5s.yaml的anchors字段。车牌场景聚类出来的 anchor 通常宽高比在 2.5 到 4 之间比如[12,36]、[18,52]这种。这一步做完小目标召回一般能涨两到三个点属于低成本高收益的操作。3. 训练 YOLOv5 车牌检测模型超参数怎么设、训练怎么盯3.1 环境配置与 data.yaml 的写法环境上PyTorch 1.8 到 2.x 都能跑 YOLOv5CUDA 版本跟显卡驱动对齐就行。conda 建环境是最省事的conda create -n yolov5_plate python3.9 conda activate yolov5_plate pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 cd yolov5 pip install -r requirements.txtdata.yaml是训练入口写清楚路径和类别path: ./plate_dataset train: images/train val: images/val nc: 1 names: [license_plate]nc是类别数车牌检测就写 1。如果加了双层车牌或新能源绿牌作为独立类改成对应数量names顺序必须和标注时的类别索引一致否则训练出来的模型会把类别搞反。3.2 训练命令与关键超参数我常用的训练命令长这样python train.py \ --img 640 \ --batch 16 \ --epochs 150 \ --data plate_dataset/data.yaml \ --weights yolov5s.pt \ --cfg models/yolov5s.yaml \ --hyp data/hyps/hyp.scratch-low.yaml \ --name plate_yolov5s \ --cache逐个说参数。--img 640是输入分辨率车牌小目标多的话可以提到 960 甚至 1280但显存和速度代价明显640 是精度和速度的平衡点。--batch 16看显存12G 显存跑 640 大概能到 16 到 24。--epochs 150是上限实际看验证集 mAP 什么时候平了通常 100 到 200 之间。--weights yolov5s.pt用官方预训练权重做迁移学习比从头训收敛快得多小数据集尤其重要。--hyp用 low 增强配置如果数据量少低于 3000 张可以换hyp.scratch-low.yaml里把mosaic关掉最后 20 个 epoch避免过拟合。--cache把图片缓存到内存训练速度能快 20% 以上数据集大就加--cache disk。3.3 训练过程盯什么指标训练日志里重点看三个box_loss、obj_loss、mAP0.5。box_loss 是框回归损失正常应该稳步下降obj_loss 是目标置信度损失如果它震荡不降多半是 anchor 不匹配或者正负样本失衡。mAP0.5 是主指标车牌检测单类场景好的模型能到 0.95 以上如果卡在 0.8 上不去先查标注质量再看数据量。验证集 mAP 涨、训练集 loss 还在降但验证 loss 开始涨就是过拟合信号这时候要么加数据要么开强增强要么早停。YOLOv5 自带--patience参数默认 100 个 epoch 没提升就停小数据集可以调到 30。3.4 用训练好的模型跑推理看效果训练完权重在runs/train/plate_yolov5s/weights/best.pt。推理命令python detect.py \ --weights runs/train/plate_yolov5s/weights/best.pt \ --source ./test_images \ --img 640 \ --conf-thres 0.4 \ --iou-thres 0.45 \ --save-txt--conf-thres 0.4是置信度阈值车牌场景我一般设 0.3 到 0.5太低会出大量误检太高会漏掉模糊车牌。--iou-thres 0.45是 NMS 的 IoU 阈值车牌之间一般不重叠0.45 够用。--save-txt把检测框坐标存下来方便后面接字符识别。跑完看runs/detect/exp里的可视化结果重点看漏检和误检分别出现在什么场景反推是数据问题还是阈值问题。4. 从检测框到车牌字符串后处理与字符识别衔接4.1 检测框裁剪与透视校正YOLOv5 输出的是水平外接矩形但实际车牌往往有倾斜直接裁出来送识别字符会变形。常见做法是先裁框再做一次透视校正。如果标注时用的是四点标注有些工具支持可以用四点做透视变换如果只有水平框可以用简单的仿射校正或者直接送识别让识别模型自己扛。import cv2 import numpy as np def crop_and_correct(img, box, expand0.05): # box: [x1, y1, x2, y2] x1, y1, x2, y2 map(int, box) h, w img.shape[:2] # 适当外扩避免裁掉车牌边缘字符 ew int((x2 - x1) * expand) eh int((y2 - y1) * expand) x1, y1 max(0, x1 - ew), max(0, y1 - eh) x2, y2 min(w, x2 ew), min(h, y2 eh) crop img[y1:y2, x1:x2] # 统一缩放到识别模型输入尺寸比如 94x24 或 168x48 crop cv2.resize(crop, (168, 48)) return cropexpand参数控制外扩比例0.05 到 0.1 之间太小会切掉边缘字符太大引入背景干扰。缩放尺寸要和后面识别模型的输入对齐常见的是 94×24CRNN 常用或 168×48。4.2 字符识别方案选型CRNN 还是轻量 CNN检测框裁出来后识别这一环有三条路一是用 CRNN CTC序列识别对不定长车牌友好是主流方案二是用轻量 CNN 做定长分类把车牌当成 7 个字符位置分别分类简单但要求车牌位置对齐好三是直接调 OCR 库比如 PaddleOCR 的车牌识别模型省事但定制性差。我一般用 CRNN因为车牌字符数不固定蓝牌 7 位、新能源 8 位、黄牌可能 7 位CTC 能处理变长序列。训练数据就是检测框裁出来的车牌图标签是车牌字符串。识别模型和检测模型分开训推理时串起来。4.3 检测与识别的串联推理把两个模型串起来的推理脚本大致长这样import torch import cv2 from models.experimental import attempt_load from utils.general import non_max_suppression # 加载检测模型 det_model attempt_load(runs/train/plate_yolov5s/weights/best.pt, map_locationcuda) det_model.eval() # 加载识别模型假设已训练好这里用占位 # rec_model ... def detect_plates(img_path): img0 cv2.imread(img_path) img cv2.resize(img0, (640, 640)) img img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, HWC to CHW img torch.from_numpy(img).float().cuda() / 255.0 img img.unsqueeze(0) with torch.no_grad(): pred det_model(img)[0] # NMS 后处理conf 0.4iou 0.45 pred non_max_suppression(pred, 0.4, 0.45) results [] for det in pred: if det is not None and len(det): for *xyxy, conf, cls in det: # 坐标还原到原图尺寸 scale img0.shape[1] / 640 box [int(x * scale) for x in xyxy] crop crop_and_correct(img0, box) # 送识别模型得到字符串 # plate_str rec_model(crop) results.append((box, conf.item())) return results逻辑上检测模型输出归一化坐标要按原图尺寸还原。non_max_suppression的阈值和 detect.py 里保持一致。识别模型接在裁剪图后面输出字符串。实际部署时两个模型可以合并成一个 ONNX 图减少 IO 开销。4.4 后处理里的字符纠错识别出来的字符串常有混淆比如0和O、1和I、8和B。车牌有固定规则第一位是汉字省份简称第二位是字母后面是字母数字组合。可以写个规则纠错# 车牌字符纠错规则 PROVINCES set(京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新) LETTERS set(ABCDEFGHJKLMNPQRSTUVWXYZ) # 车牌不含 I 和 O def correct_plate(s): if not s: return s chars list(s) # 第一位必须是省份汉字 if chars[0] not in PROVINCES: # 尝试从常见混淆里恢复这里简化处理 pass # 后续位把 O 换成 0I 换成 1 for i in range(1, len(chars)): if chars[i] O: chars[i] 0 elif chars[i] I: chars[i] 1 return .join(chars)这个纠错规则能救回不少识别错误尤其是数字和字母混淆的场景。规则要根据你的实际车牌类型调整新能源车牌位数和规则都不一样。5. 车牌识别落地避坑五个我踩过的坑5.1 坑一训练 mAP 很高实际部署漏检严重现象验证集 mAP0.5 到 0.97但拿现场视频流跑漏检率超过 20%。原因验证集和训练集同分布都是清晰卡口图但现场有逆光、雨雾、夜间红外。模型没见过的域泛化直接崩。解决训练集里必须混入目标场景的图哪怕只有几百张。另外推理时把--conf-thres从 0.4 降到 0.25配合后处理规则过滤误检召回优先。如果现场光照变化大加一个简单的图像预处理比如 CLAHE 做自适应直方图均衡。5.2 坑二anchor 没重聚类小目标召回上不去现象训练 loss 正常降但小尺寸车牌远距离、低分辨率召回率明显低于大车牌。原因默认 COCO anchor 的宽高比和车牌不匹配小 anchor 尺寸也不对正样本匹配时小目标很难被分配到合适的 anchor。解决跑utils/autanchor.py聚类自己的数据把结果填进模型配置。这一步做完小目标召回通常能涨 3 到 5 个点。聚类时--img-size要和训练时一致。5.3 坑三数据增强开太猛车牌字符被裁掉现象训练时 loss 震荡验证集精度上不去可视化发现有些训练样本车牌被裁了一半。原因YOLOv5 默认的 Mosaic 和随机裁剪增强对普通目标没问题但车牌字符密集裁掉一部分字符后标签还是整个车牌框模型学到的是不完整特征。解决在hyp.scratch-low.yaml里把mosaic的概率调低或者最后 20 个 epoch 关掉 Mosaic。随机缩放的范围也收窄scale从默认的 0.5 调到 0.3。数据增强是为了泛化但不能破坏标签语义。5.4 坑四检测框裁太紧识别模型丢字符现象检测框可视化看着很准但识别出来的车牌字符串总是少一两个字符尤其是边缘的汉字。原因检测框紧贴车牌但识别模型训练时的裁剪图通常带一点边距推理时裁太紧字符被切掉边缘笔画。解决裁剪时做外扩expand参数设 0.05 到 0.1。另外检测框本身可以稍微放宽训练标注时留 1 到 2 像素边距让模型学到的框略大于车牌。5.5 坑五部署时 FP16 量化导致小目标检测崩掉现象PyTorch 模型推理正常转 TensorRT FP16 后小目标检测精度明显下降。原因FP16 精度对小目标的特征响应不友好尤其是车牌这种小面积目标量化误差放大。解决小目标场景优先用 FP32 或 INT8 校准如果必须 FP16做量化感知训练或者在 TensorRT 里对检测头部分保留 FP32。部署前一定要用同一批测试图对比量化前后的 mAP别只看速度。6. 把 YOLOv5 车牌识别压到端侧树莓派 5 上的部署取舍端侧部署是很多人关心的方向尤其是树莓派 5 这类设备。我实际跑过一轮说几个关键取舍。首先是模型选择YOLOv5s 在树莓派 5 上直接跑 PyTorch 大概 2 到 3 FPS没法用。必须转格式常见路线是 PyTorch → ONNX → NCNN 或 ONNX Runtime。NCNN 在 ARM 上优化好YOLOv5s 转 NCNN 后能到 8 到 12 FPS勉强够单路视频。如果还要接识别模型帧率会再降所以端侧一般只做检测识别放后端或者用极轻量的识别模型。转换命令大致是# PyTorch 转 ONNX python export.py --weights best.pt --include onnx --img 640 --batch 1 # ONNX 转 NCNN需要 onnx2ncnn 工具 onnx2ncnn best.onnx best.param best.bin转完注意输入输出层的名字NCNN 推理时按名字取。输入尺寸可以降到 416 甚至 320速度能再提一截但小目标召回会掉需要重新评估。另一个取舍是输入分辨率。树莓派 5 上 640 输入跑 NCNN 大概 10 FPS降到 416 能到 18 FPS 左右但远距离车牌就检测不到了。我的做法是双分辨率策略先用 416 快速筛一遍有候选框的区域再裁出来用 640 精检兼顾速度和召回。最后是内存和散热。树莓派 5 跑持续推理不加散热片会降频帧率掉一半。加个风扇把 CPU governor 设成 performance 模式能稳住。模型权重放内存盘减少 SD 卡 IO。这套方案值不值得做取决于你的场景。如果是固定点位、光照可控、车牌距离近端侧完全可行如果是多场景、远距离、高召回要求还是老老实实上服务器 GPU端侧只做粗筛。我自己踩过的最大教训是别在端侧追求和服务器一样的精度先把速度跑通再按场景调阈值精度不够的地方用后端补。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →