YOLOv7车牌识别实战:12类中文号牌检测与CRNN识别全流程
发布时间:2026/9/28 1:17:25来源:尧图网络
简介这是一套基于YOLOv7的车牌识别算法系统面向计算机、人工智能、通信工程、自动化等专业的在校学生、教师及企业开发者可用于毕业设计、课程设计、项目立项演示或算法进阶学习。系统支持单行蓝牌、单行黄牌、新能源车牌、白色警用车牌、教练车牌、武警车牌、使馆车牌、港澳牌车、双层农用车牌、民航车牌等12种中文车牌类型覆盖常见与特殊场景。资源包共100个文件约24.05MB包含38个Python源码、13个YAML配置、5个Shell脚本、4个C与1个CUDA文件以及2个pth、2个pt权重文件另附jpg、png效果图与md、docx、pptx说明文档兼顾训练、推理与部署演示。项目代码经过测试运行成功并获导师认可答辩评审分达96分。已有122人学习关注读者可获取完整源码、模型权重与配置脚本快速复现车牌检测识别流程并在此基础上修改扩展功能。1. 从一张车牌到 12 类中文号牌yolov7 车牌识别到底在解决什么城市路侧停车、园区道闸、加油站车牌付这些场景里最常被问的一句话是能不能用一套 yolov7 车牌识别算法把蓝牌、黄牌、绿牌、白牌、黑牌、民航、使馆、领馆、港澳、教练、警用、军车这 12 种中文车牌类型都覆盖掉答案是能但前提是你得先想清楚两件事——检测和识别是两段独立任务以及中文车牌的字符集和版式差异远比 COCO 数据集里那些猫猫狗狗复杂。很多人第一次上手直接拿 yolov7 官方权重去跑车牌结果检测框能出来字符却认得一塌糊涂这就是把检测当识别用的典型翻车。这套「文档源码」形态的方案本质是给你一条从数据标注、yolov7 训练、字符识别头接续到推理部署的完整链路适合两类人一类是想快速跑通 demo 验证可行性的算法工程师另一类是要把车牌识别嵌进自己停车/安防系统的后端开发。它不解决「一张模糊夜间图也能 99% 准确」这种玄学问题但能让你在标准卡口场景下把 12 类中文号牌的检测识别做到可用。下面我按自己实际搭过一遍的顺序把选型理由、训练参数、避坑点和验证方法讲透你照着复现基本不会卡在环境上。2. yolov7 车牌检测为什么选它而不是 v5/v8以及最小训练闭环2.1 检测与识别为什么要拆成两阶段车牌识别这个任务业内的常见做法是「检测 识别」两阶段而不是端到端一个网络出字符串。原因很直接车牌在整张图里占比小直接做序列识别背景干扰会让 CTC 或 Attention 解码极不稳定而先检测出车牌四角点再做透视变换裁正识别阶段拿到的就是一张规整的小图字符分割和序列建模都简单得多。yolov7 在这里承担的是检测器角色输出车牌框顺带可以回归四个角点做矫正。选 yolov7 而不是 yolov5 或 yolov8主要看三点。第一yolov7 的 E-ELAN 结构和辅助训练头aux head在小目标上召回比 v5 稳车牌这种小目标密集场景收益明显第二yolov7 的 anchor 机制和正样本分配策略对长宽比极端的车牌框比如黄牌大车 440×140更友好第三社区里车牌微调的现成配置多改yolov7.yaml的nc就能接自己的类别。yolov8 虽然 API 更顺手但如果你手上是旧版训练脚本和权重迁移成本反而更高。所以这套方案锁 yolov7 是有工程道理的不是跟风。2.2 数据标注格式与 12 类车牌的分类策略12 种中文车牌类型检测阶段有两种标法一种是把所有车牌当 1 类plate类型判断交给识别阶段或后处理另一种是直接标 12 类让检测器同时输出类型。我一般推荐前者因为类型之间的视觉差异比如蓝牌和黑牌在检测尺度下并不总是可分强行 12 类会让 mAP 掉得很难看。类型识别放到识别阶段用字符首字或颜色特征判断更稳。标注用 labelImg 或 X-AnyLabeling 出 YOLO 格式 txt每行class x_center y_center width height归一化到 0~1。如果你要角点矫正就额外存四点坐标转成透视变换矩阵。数据量上单类车牌检测想收敛至少准备 3000~5000 张覆盖白天/夜间/逆光/雨天的图12 类里稀有类型使馆、领馆、民航每类不少于 200 张否则检测器会偏向高频类。# 目录结构约定yolov7 训练脚本默认按这个找数据 datasets/ plates/ images/ train/ val/ labels/ train/ val/ # data/plate.yaml 内容 train: ../datasets/plates/images/train val: ../datasets/plates/images/val nc: 1 names: [plate]这段配置里nc: 1就是单类检测的关键names只写plate。如果你坚持 12 类把nc改成 12 并列出中文名拼音但要做好 mAP 下降 5~10 个点的心理准备。路径用相对路径避免换机器后找不到数据。2.3 用 yolov7 训练车牌检测器的最小命令与参数训练命令本身不复杂难的是参数怎么设。车牌检测我一般用yolov7.yaml作为基础结构输入尺寸 640batch 根据显存调8G 显存跑 batch 16 比较稳。python train.py \ --workers 8 \ --device 0 \ --batch-size 16 \ --data data/plate.yaml \ --img 640 640 \ --cfg cfg/training/yolov7.yaml \ --weights yolov7.pt \ --name plate_yolov7 \ --hyp data/hyp.scratch.p5.yaml \ --epochs 150 \ --nosave --cache--weights yolov7.pt是 COCO 预训练权重迁移学习能省一半以上 epoch--hyp用 p5 的默认超参学习率 lr0 默认 0.01如果你数据量小于 3000 张建议手动把 lr0 降到 0.001否则容易过拟合--cache把图缓存进内存加速但数据超过 1 万张时别开会吃爆内存--nosave只存最后和最优权重省磁盘。训练过程中重点看mAP0.5和mAP0.5:0.95车牌检测 mAP0.5 到 0.95 以上基本可用低于 0.9 先回去查标注有没有漏框、框有没有贴边。2.4 推理阶段怎么把检测框裁出来送识别训练完拿到best.pt推理时用detect.py或自己写脚本调torch.load。关键一步是把检测框按坐标裁图并做 padding避免字符被切掉。import cv2, torch from models.experimental import attempt_load from utils.datasets import letterbox from utils.general import non_max_suppression model attempt_load(runs/train/plate_yolov7/weights/best.pt, map_locationcpu) img cv2.imread(test.jpg) im0 img.copy() im letterbox(img, 640, stride32)[0] im im[:, :, ::-1].transpose(2, 0, 1) im torch.from_numpy(im).float() / 255.0 im im.unsqueeze(0) pred model(im)[0] pred non_max_suppression(pred, conf_thres0.25, iou_thres0.45) for det in pred: if det is not None: for *xyxy, conf, cls in det: x1, y1, x2, y2 map(int, xyxy) # 外扩 5 像素防止字符贴边被裁掉 pad 5 crop im0[max(0,y1-pad):y2pad, max(0,x1-pad):x2pad] cv2.imwrite(crop.jpg, crop)conf_thres0.25是检测置信度阈值车牌场景可以放到 0.3 减少误检iou_thres0.45控制 NMS 重叠车牌密集时别调太低否则相邻车牌会被吞。裁图外扩 5 像素是血泪经验很多识别错误就是因为字符边缘被切了半个。3. 字符识别头12 类中文车牌的序列建模与训练3.1 中文车牌字符集到底有多少类中文车牌不是纯数字字母它包含省份汉字京沪粤苏等 31 个、字母A-Z 去掉 I 和 O、数字0-9再加上新能源绿牌的特殊位、警用「警」、军车汉字、使馆「使」、领馆「领」、港澳「港」「澳」、教练「学」、民航等。全字符集加起来大概 70~80 类。识别头如果用 CTC输出维度就是字符集大小 1blank如果用 Attention 解码就是字符集大小 start/end。这里有个容易踩的坑很多人直接拿 CRNN 的英文数字权重来微调结果汉字全认不出。正确做法是从头训识别头或者用中文通用 OCR 权重比如 PP-OCR 的中文识别模型做初始化再在车牌数据上微调。字符集一定要在训练前固定下来写进charset.txt推理时按同一顺序解码否则会出现「训练时第 5 类是『京』推理时第 5 类变成『沪』」这种低级错误。3.2 CRNN 识别头的结构与输入尺寸识别头我一般用 CRNNCNN 提特征 BiLSTM 做序列 CTC 解码。输入尺寸固定为 32×100高×宽车牌裁正后 resize 到这个尺寸。CNN 部分可以用 MobileNetV3 或 ResNet18 的 backbone轻量且够用。import torch.nn as nn class CRNN(nn.Module): def __init__(self, num_classes): super().__init__() # 简化 backbone实际可用 mobilenetv3 self.cnn nn.Sequential( nn.Conv2d(3, 64, 3, 1, 1), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(64, 128, 3, 1, 1), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(128, 256, 3, 1, 1), nn.ReLU(), ) self.rnn nn.LSTM(256, 128, bidirectionalTrue, batch_firstTrue) self.fc nn.Linear(256, num_classes) def forward(self, x): # x: B,3,32,100 feat self.cnn(x) # B,256,H/4,W/4 B, C, H, W feat.shape feat feat.permute(0, 3, 1, 2).reshape(B, W, C * H) out, _ self.rnn(feat) return self.fc(out) # B, T, num_classesnum_classes就是字符集大小CTC 不需要额外 blankPyTorch 的CTCLoss会自动处理。输入高 32 是 CRNN 的经典设置宽 100 对应车牌比例如果你的车牌特别长黄牌大车可以把宽调到 128。BiLSTM 隐藏层 128 是平衡精度和速度的选择再大推理会慢。3.3 识别训练的数据增强与 CTC 损失配置识别阶段的数据增强和检测不一样不能随便旋转因为字符方向敏感。我一般用亮度对比度扰动、轻微透视变换、高斯噪声不做大角度旋转。标签是字符串训练时转成索引序列。import torch from torch.nn import CTCLoss charset open(charset.txt, encodingutf-8).read().strip().split(\n) char2idx {c: i for i, c in enumerate(charset)} ctc_loss CTCLoss(blanklen(charset), zero_infinityTrue) def encode(label): return torch.tensor([char2idx[c] for c in label], dtypetorch.long) # 假设 logits: B,T,C, targets 是拼接后的索引, target_lengths 是每张图字符数 loss ctc_loss(logits.log_softmax(2).permute(1, 0, 2), targets, input_lengths, target_lengths)blanklen(charset)把 blank 放在最后一类zero_infinityTrue防止某些样本长度不匹配导致 loss 变 inf。input_lengths是 T时间步target_lengths是真实字符数CTC 要求 T 大于等于字符数所以输入宽 100 对应 T 大概 25足够放 7~8 个字符。如果 loss 一直不降先检查targets拼接顺序和target_lengths对不对这是最常见的翻车点。3.4 把检测和识别串成完整推理管线两阶段都训好后推理管线就是读图 → yolov7 检测 → 裁图矫正 → CRNN 识别 → 按类型规则后处理。后处理这步别省比如新能源绿牌是 8 位蓝牌是 7 位识别出 6 位或 9 位直接判为无效警用牌末尾是「警」如果识别成其他字可以按规则纠正。def recognize(crop, rec_model, charset): img cv2.resize(crop, (100, 32)) img img.transpose(2, 0, 1)[None] / 255.0 logits rec_model(torch.from_numpy(img).float()) pred logits.argmax(2)[0].tolist() # CTC 去重去 blank result, prev [], -1 for p in pred: if p ! prev and p ! len(charset): result.append(charset[p]) prev p return .join(result)CTC 解码的去重逻辑是「相邻相同合并blank 分隔」这里prev记录上一个索引p ! len(charset)过滤 blank。实际部署时建议用torch.no_grad()包住推理速度能快 20% 以上。4. 部署与性能yolov7 车牌识别上线的几个现实问题4.1 ONNX 导出与 TensorRT 加速的取舍训练用 PyTorch部署一般转 ONNX 再转 TensorRT。yolov7 导出 ONNX 时注意--grid参数否则动态 shape 会出问题。CRNN 导出 ONNX 相对简单但 LSTM 在 TensorRT 里支持有限如果遇到不支持可以把 BiLSTM 换成卷积序列建模或者直接用 ONNX Runtime 跑识别头。python export.py --weights best.pt --grid --end2end --simplify \ --topk-all 100 --iou-thres 0.45 --conf-thres 0.25 --img-size 640 640--end2end会把 NMS 也导进去省得后处理用 Python 写--simplify用 onnx-simplifier 精简图结构。TensorRT 加速后 640 输入在 T4 上单图检测能到 5ms 以内识别头 2ms 左右整条管线 10ms 内满足实时视频流。4.2 批量推理与视频流处理的工程细节视频流场景别一帧一帧调用 batch 推理能显著提吞吐。但车牌检测有个特点不是每帧都有新车牌可以用简单跟踪比如 IOU 匹配减少重复识别同一辆车牌连续帧只识别一次后处理缓存结果。这样 GPU 利用率能降一半。# 伪代码跳帧 跟踪缓存 tracker {} for frame_id, frame in enumerate(stream): if frame_id % 3 ! 0: # 每 3 帧检测一次 continue dets detect(frame) for det in dets: tid match_tracker(det, tracker) if tid not in tracker or tracker[tid][age] 10: tracker[tid] {text: recognize(det), age: 0} else: tracker[tid][age] 1跳帧间隔根据车速调卡口场景车速快可以每帧都检园区低速可以 3~5 帧一次。跟踪缓存要设过期时间否则车牌离开画面后缓存还在会输出幽灵结果。4.3 12 类车牌类型判断放在哪一层前面说过检测只出plate一类类型判断放识别后。具体规则先看颜色蓝、黄、绿、白、黑再看字符首字使、领、警、学、军等最后看位数新能源 8 位。颜色判断用 HSV 阈值就够别上分类网络增加延迟还不一定准。港澳牌比较特殊黑底白字和普通黑牌容易混可以结合字符里有没有「港」「澳」来区分。5. 避坑与排查车牌识别从训练到上线最容易翻车的 5 个点5.1 现象训练 mAP 很高实测漏检严重原因通常是训练集和实测场景分布不一致比如训练全是白天图实测是夜间红外。解决方法是补夜间数据或者在训练时加亮度扰动增强把hsv_v调大。另外检查实测图的分辨率如果训练用 640 而实测图是 4K 下采样小目标会丢推理时把输入尺寸提到 1280 试试。5.2 现象识别结果汉字全错数字字母对原因是识别头用了英文权重初始化汉字没学到。解决方法是换中文 OCR 权重初始化或者从头训并确保charset.txt里汉字编码是 UTF-8。还有一种可能是训练标签里的汉字和charset.txt不一致比如标签是「京」而 charset 里是「京A」对不上就全错。5.3 现象CTC loss 变 NaN 或 inf检查target_lengths是否大于input_lengthsCTC 要求 T ≥ 标签长度。另外zero_infinityTrue要开否则个别样本会让整个 loss 爆掉。如果还不行把学习率降到 1e-4 再试CTC 对学习率比较敏感。5.4 现象ONNX 导出后推理结果和 PyTorch 不一致常见原因是导出时没加--simplify或者动态轴设置不对。yolov7 导出要加--gridCRNN 导出注意 LSTM 的 batch 维。对比时用同一张图先跑 PyTorch 存下输出再跑 ONNX逐层对比一般问题出在 NMS 或 reshape 的维度顺序上。5.5 现象多车牌场景只识别出一个NMS 的iou_thres设太低相邻车牌被合并了。车牌密集场景把iou_thres提到 0.5~0.6或者改用 soft-NMS。另外检测框外扩 padding 太大也会导致两个框重叠padding 控制在 5~10 像素。6. 进阶技巧用角点回归 透视变换把识别率再拉一截前面讲的都是水平框检测但实际卡口相机有俯角车牌是倾斜的水平框裁出来的图带大量背景识别率上不去。进阶做法是让 yolov7 额外回归车牌四个角点然后做透视变换把车牌拉正。这一步能把识别准确率提升 3~5 个点尤其是大角度场景。具体实现在检测头加一个 8 维输出4 个点 x,y损失用 Wing Loss 或 Smooth L1。训练数据标注时除了水平框再标四个角点。推理时拿到角点后用cv2.getPerspectiveTransform算变换矩阵把车牌映射到 100×32 的标准图再送识别。import numpy as np, cv2 def perspective_crop(img, pts): # pts: 4 个角点顺序 左上 右上 右下 左下 dst np.float32([[0,0],[100,0],[100,32],[0,32]]) M cv2.getPerspectiveTransform(np.float32(pts), dst) return cv2.warpPerspective(img, M, (100, 32))角点顺序一定要固定训练和推理一致否则变换出来是镜像或旋转的。如果角点回归不稳定可以先跑水平框检测再用分割或边缘检测找角点两条路都行看你的数据标注成本。验证这套方案值不值得做我的习惯是先用 500 张真实场景图跑一遍端到端统计检测召回和识别整牌准确率。整牌准确率所有字符全对能到 90% 以上这套 yolov7 车牌识别方案就值得往生产推低于 80% 先回去查数据和角点别急着上 TensorRT。我自己踩过最大的坑是忽略角点矫正水平框在俯角场景下整牌准确率只有 76%加了透视变换直接到 93%这一步的性价比比换 backbone 高得多。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网