ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

YOLOv7+CRNN车牌识别实战:从训练到部署的完整指南

YOLOv7+CRNN车牌识别实战:从训练到部署的完整指南 简介基于YOLOv7加CRNN的车牌检测与中文车牌识别完整项目专门面向正在进行毕业设计、课程设计或需要项目实战的深度学习视觉图像识别学习者可用于快速搭建车牌识别系统并完成实验验证。压缩包共192个文件整体约55.64MB涵盖99个Python脚本、15个YAML配置文件、PyTorch权重文件、图片样本、Shell脚本及C和CUDA部署源码等从数据准备、模型训练到推理部署均有涉及目录结构清晰便于按功能模块学习。项目已提供训练好的模型和详细操作说明能够直接运行并识别国内中文车牌同时附带数据集与标注文件便于深入理解YOLOv7目标检测和CRNN序列识别原理也可在此基础上扩展训练其他模型。目前已有1276人学习下载对于需要完整参考项目的学习者来说是一份可落地、可复用的优质资源。1. 车牌识别不是“检测 读字”那么简单为什么偏偏是 YOLOv7 CRNN中文车牌识别是一个很有代表性的工业视觉任务它不要求极致的精度但要求稳定、可部署、好调试。很多人以为把目标检测和 OCR 拼起来就能做结果一跑才发现车牌在画面里往往只有几十个像素而且可能有倾斜、反光、残缺。用 YOLOv7 负责定位“车牌在哪”再用 CRNN 负责识别“牌上是什么”是过去几年在私有环境里最容易复现、也最容易把坑讲明白的组合。这个方案的好处是检测和识别可以单独优化检测错了不会怪识别识别错了也不用重训检测模型。本文基于“代码 数据集 说明文档”这种工程包把从数据准备到推理部署的关键环节过一遍适合已经跑过 YOLOv5、想进一步接触 OCR 和序列识别的工程师。2. YOLOv7 车牌检测模型以数据为中心的训练思路真正做过车牌检测的人都知道难点不在网络结构而在训练数据怎么组织。YOLOv7 是成熟的目标检测器官方权重能识别 80 类日常物体但车牌这种长宽比接近 3:1、又容易出现小目标的目标必须用专用数据集重新训练。下面按源码工程的标准流程来说。2.1 车牌检测任务和通用物体检测差在哪通用检测里目标通常是方方正正的物体比如人、车、狗。车牌不一样它在图片中的形态很极端尺寸小一辆车在 1080p 画面里可能占 1/3但车牌只占其中一小块长宽比固定且极端蓝牌比例约 440:140新能源车牌 480:140透视形变来源可能是停车场闸机俯视、道路卡口斜视车牌可能是个梯形背景干扰车灯、进气格栅、保险杠的纹理和车牌颜色接近。这些特点决定了训练时的三个关键决策一是输入分辨率不要死守 640可以提到 960 或 1280二是 anchor 尺寸要重新聚簇不能沿用 COCO三是数据增强里要加重随机旋转和透视变换模拟不同视角。2.2 数据标注与目录结构VOC、COCO 还是 YOLO 格式源码包里的“数据集”目录通常不外乎以下几种结构。最常见的是 YOLO 格式每个图片对应一个同名 txt每行是class_id cx cy w h坐标都归一化到 0~1。工程里你会看到类似这样的 layersdatasets/ ├── annotations/ # 如果有 VOC/COCO 标注会在这里 ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamldata.yaml是 YOLOv7 训练时读的数据配置。对于一个只识别车牌的项目里面的内容一般是train: datasets/images/train val: datasets/images/val nc: 1 names: [license_plate]对源码做一些修改时要注意YOLOv7 的datasets.py会按data.yaml里的路径去加载图片和标签。如果你拿到的是 VOC 标注JPEGImagesAnnotations我一般会先用voc_label.py转成 YOLO 格式再开始训练避免在复现时被格式问题卡住。2.3 修改模型配置anchor 必须重新计算YOLOv7 的官方cfg/training/yolov7.yaml里默认 anchor 是针对 COCO 数据集聚类出来的长宽比从 0.6 到 6但车牌这种长宽比 3 左右的对象其实用默认 anchor 也能训只是收敛慢、回归不精准。比较稳妥的做法是用 k-means 对训练集中的所有真实框重新聚类。修改方法是在源码utils/autoanchor.py里跑一次聚类然后替换yolov7.yaml中的anchors段。以一个车牌数据集为例聚类出的 9 个 anchor 可能是anchors: - [10, 6, 15, 9, 24, 12] # P3 特征图 - [32, 18, 54, 24, 88, 36] # P4 特征图 - [120, 50, 180, 80, 280, 120] # P5 特征图注意车牌虽然整体长宽比固定但在不同尺度下锚框的宽高可以保持 2.5:1 到 3.5:1。重新聚类后训练时 mAP 提升虽然可能只有 12 个点但收敛更快尤其在小目标上是质的变化。2.4 训练命令与必调参数训练入口是train.py。一个适合单卡 RTX 3080 的常用命令如下python train.py \ --weights yolov7.pt \ --data data/license.yaml \ --hyp data/hyp.scratch.custom.yaml \ --batch-size 16 \ --img 960 \ --epochs 150 \ --workers 8 \ --device 0每个参数都有实际意义。--weights yolov7.pt是 COCO 预训练权重车牌虽然不属于 COCO 类别但预训练模型已经学到了边缘、纹理等底层特征迁移学习比从头训练效果好得多。--img 960是因为车牌小把输入分辨率从默认 640 提到 960小目标召回能涨不少代价是显存占用增加batch 降到 16 是折中。--hyp传自定义增强参数重点是开启mosaic1.0、hsv_h0.015、degrees10其中degrees控制旋转角度10 度对车牌已经够用太大容易让车型姿态失真。训练过程中要盯两组指标P/R 曲线和验证集 loss。如果 precision 高但 recall 低说明模型只认得出“正正的蓝色车牌”需要增加斜视角度的样本如果 loss 下降很快但 mAP 上不去优先怀疑 anchor 没更新。一个常见误用是直接把 COCO 的hyp.scratch.yaml拿过来里面degrees0不做旋转增强导致实拍场景一概漏检。2.5 用 mAP 衡量模型时注意 box 坐标的绝对误差YOLOv7 测试会输出 mAP0.5 和 mAP0.5:0.95。对车牌任务来说mAP0.5 高于 0.98 不代表完美因为 0.5 的 IoU 阈值对“检测框只占车牌一半”这种结果也会判为正检。车牌后续要裁剪做识别框偏了直接导致识别输入缺字。所以在评估时我一般会把--iou-thres和--conf-thres调出来单独打印预测框和真实框的偏差重点关注 IoU 在 0.75 以上的比例。真正落地的模型应保证 mAP0.75 在 0.9 以上否则识别准确率会被检测框的“差不多”拖垮。3. CRNN 中文车牌识别用 CTC 处理变长序列检测模型输出的是一块“疑似车牌”的图像接下来要识别出车牌上的字符。中文车牌的特殊之处在于字符序列固定但字符集很大第一位是省份汉字如“粤”“京”“鄂”第二位是发牌机关字母后面是字母和数字新能源车牌比普通蓝牌多一位。CRNN 是目前工程上比较通用的方案。3.1 为什么用 CRNN 而不是 CNN 分类常规 CNN 分类要求输入图像类别数量固定比如把车牌分成“粤A12345”这种完整字符串类别数取决于可能组合分散且组合爆炸实际不可行。一种替代思路是“定长多分类”把车牌切成 7 段每段各自分类例如 7 个分类器分别输出汉字、字母、数字。这样做的问题是切割位置难定而且车牌字符并不均匀排列一旦检测框倾斜切出来的字符就错位。CRNN 的关键是允许模型“一口气”读完整张图输出一个序列然后通过 CTC Loss 自动对齐到真实标签。它不需要显式切字符只需要输入“包含一段文字”的图像网络自己决定每个时间步对应哪个字符。这使得它天然适合车牌这种长度略有变化、字符分布不均匀的识别任务。3.2 字符集设计与标签编码中文车牌识别要先确定字符集。通常包括省份简称京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼 字母A~Z除去 I、O 等容易混淆的 数字0~9字符集长度一般在 60 左右。太多没有必要因为车牌上只会出现这些字符。在源码工程里常见的做法是把字符集写在一个char_dict.txt中按行编号例如0 京 1 津 2 沪 ...标签编码时每个标签是字符索引的序列。CRNN 训练时输入是整张车牌图例如高 32、宽 96输出是长度为 T 的特征序列最终通过 softmax 得到每个时间步在字符集上的概率分布。3.3 基于 PyTorch 构建一个最小 CRNN 训练脚本假设你已经将车牌图片裁剪成正角度并缩放到 32×168高×宽我们可以用下面的代码片段定义数据加载核心逻辑import torch import torch.nn as nn from torch.utils.data import Dataset class PlateDataset(Dataset): def __init__(self, img_paths, labels, char_dict, height32, width168): self.img_paths img_paths self.labels labels self.char_dict {c: i for i, c in enumerate(char_dict)} self.height height self.width width def __len__(self): return len(self.img_paths) def __getitem__(self, idx): # 实际应用中用 cv2.imread 和 resize img load_and_resize(self.img_paths[idx], self.height, self.width) img torch.from_numpy(img).float().permute(2, 0, 1) / 255.0 # 标签转为索引列表例如 [20, 3, 15, ...] target [self.char_dict[c] for c in self.labels[idx]] target_len torch.tensor(len(target), dtypetorch.long) target_tensor torch.tensor(target, dtypetorch.long) return img, target_tensor, target_len代码里需要注意char_dict必须与最后模型全连接层的输出维度一致。CRNN 模型结构一般是“CNN 卷积特征提取 → RNNLSTM/GRU → 全连接分类”最终输出的特征序列长度为W / 4左右比如输入宽 168经过 4 次下采样后序列长度是 42那么 RNN 输出的每个时间步就对应图上大约 4 像素宽的滑动窗口。训练时用 CTC Lossimport torch.nn.functional as F from torch.nn import CTCLoss criterion CTCLoss(blank0) # blank 是 CTC 用于“空白帧”的类别 optimizer torch.optim.Adam(model.parameters()) for epoch in range(epochs): for imgs, targets, target_lens in dataloader: logits, logit_lens model(imgs) # logits shape: [batch, seq_len, num_classes] loss criterion(logits.transpose(1, 0), targets, logit_lens, target_lens) optimizer.zero_grad() loss.backward() optimizer.step()这里logits的类别数比字符集数多 1多的一个就是 CTC 的 blank。解码时把模型输出的概率序列取 argmax然后“合并重复字符并去掉 blank”就能得到车牌字符串。这种解码方式叫“贪心解码”速度最快也是工程里最常用的。3.4 CRNN 训练的参数陷阱图像高度和字符间距CRNN 对输入高度很敏感因为卷积下采样的倍率是固定的。如果你把车牌图像直接 resize 成任意高度会导致竖直方向的字符被压缩或拉伸RNN 读不到足够的细节。常见做法是固定高度为 32宽度按原始宽高比等比缩放不是直接拉宽。如果源车牌是从检测框裁出来的长宽比可能被破坏最好先做透视校正再缩放。另外CTC 训练时不要把所有图片都缩放到同一固定宽度这样会丢失字符分布信息。更稳妥的做法是在一个 batch 内做 padding设置最大宽度然后动态 mask。很多开源项目偷懒直接resize(32, 168)训练出来的模型遇到“宽度比例异常”的车牌比如新能源 7 位时末尾字符经常丢。我会建议按原图比例缩放后再用零填充到统一宽度。4. 把检测和识别串起来推理管线与工程化细节训练完两个模型真正跑业务时要做的事比训练更多检测框怎么转成识别输入识别概率怎么过滤两个模型怎么组织成一个稳定的调用链。这一节给出一个可以抄走的推理管线。4.1 从检测框到车牌图像透视校正与裁剪YOLOv7 检测输出的是矩形框x1, y1, x2, y2但实际车牌可能旋转倾斜。如果直接裁剪矩形框往往带进车灯、车漆等背景影响识别。更好的做法是使用 OpenCV 的minAreaRect先提取车牌的旋转外接矩形再做透视变换。import cv2 import numpy as np def crop_plate_rect(image, box): box: [x1, y1, x2, y2] YOLO检测结果 返回校正后的车牌图像 roi image[int(box[1]):int(box[3]), int(box[0]):int(box[2])] # 转为灰度后找轮廓 gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) edges cv2.Canny(gray, 100, 200) contours, _ cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if len(contours) 0: return roi # 选择最大轮廓 cnt max(contours, keycv2.contourArea) rect cv2.minAreaRect(cnt) # rect 的宽高其中一个是车牌的“高” w int(rect[1][0]) h int(rect[1][1]) if w h: w, h h, w # 定义目标坐标按宽度为长边 dst np.array([[0, 0], [w, 0], [w, h], [0, h]], dtypenp.float32) # 注意 rect 的四个角点顺序需要调整 box_pts cv2.boxPoints(rect) # 按坐标排序对齐到 dst src order_points(box_pts) # 自己实现角点排序 M cv2.getPerspectiveTransform(src, dst) warped cv2.warpPerspective(image, M, (w, h)) return warped这个函数里order_points需要把最小外接矩形的四个顶点统一为“左上、右上、右下、左下”的顺序否则透视变换会得到翻转或扭曲的结果。实际工程中如果检测框本身已经比较准IoU 0.85 以上也可以不做轮廓校正只把裁剪区域等比缩放这样更快但识别率略低。我一般优先保留透视校正因为它的耗时增加不到 2ms但对中文车牌的末位字符识别提升明显。4.2 识别后处理贪心解码与置信度过滤CRNN 输出一个形状为[batch, seq_len, num_classes]的概率矩阵。解码时每个时间步取最大概率类别再去重去 blank。下面是一段简洁的实现def decode_ctc(preds, char_dict): preds: [seq_len, num_classes] indexes preds.argmax(dim-1) # 每个时间步的类别 id prev -1 result [] for idx in indexes.tolist(): c char_dict[idx] if c _: # 假定 blank 的字符标识为 _ prev -1 continue if c ! prev: result.append(c) prev c return .join(result)这样解码出的字符串就是候选车牌。注意prev的去重逻辑CTC 允许同一个字符连续出现两次但车牌上不会连续出现两个相同字符所以简单的“相同字符合并”是合适的。如果遇到“粤B88888”这种连续 5 个 8CTC 合并后只剩下一个 8那就是识别错误。实际中这个情况不多但最好在后处理里加上语法校验第一位是汉字第二位一定是大写字母后面 4 到 5 位是字母或数字。不满足时可以用置信度较低但结构合法的候补结果。4.3 完整的单张图片推理流程把检测和识别封装成一个函数方便在服务或脚本中调用def detect_and_recognize(image): # 1. 检测 boxes, scores yolo_detect(image, conf_thres0.3, iou_thres0.45) results [] for box, score in zip(boxes, scores): warped crop_plate_rect(image, box) warped cv2.resize(warped, (168, 32)) # 2. 识别 preds crnn_model(warped) # [seq_len, num_classes] plate decode_ctc(preds, char_dict) results.append({ box: box, confidence: score, plate: plate }) return results这个函数看起来简单但有三个细节会影响整体准确率conf_thres别设成 0.5 以上。车牌检测模型经过车牌数据集训练后置信度普遍偏高但漏检一张的代价比误检大建议 0.250.35iou_thres保持 0.45 即可没必要调到 0.5因为车牌之间不会重叠只有误检时才会出现高 IoU 重复框识别前要把warped做归一化/255.0并保证三个通道顺序与训练时一致。很多人直接在 PyTorch 里用transpose(2,0,1)却忘了除以 255导致识别率骤降 20%。4.4 推理管线的常见参数调节表下表是实际项目中比较常用的参数范围供调试时参考参数推荐值调节方向det conf_thres0.25 ~ 0.35漏检多就调低误检多就调高det iou_thres0.45固定即可检测输入尺寸960小目标多就提高但注意显存识别输入高度32不能随意改需匹配模型下采样倍数识别输入宽度按宽高比缩放后 pad不要固定拉宽识别置信度过滤无语法校验优先引擎不行才考虑过滤这个表也是我做故障排查时优先看的一层。如果检测框没框住车牌问题一定在检测参数或训练数据而不是识别如果检测框准但识别字符串非法才去调 CRNN 输入和后处理。5. YOLOv7 部署与识别性能优化从 ONNX 到 TensorRT识别模型训练好不加优化地直接用 PyTorch 跑速度往往只有几十 FPS在卡口摄像头或边缘盒子上根本不够。把 YOLOv7 和 CRNN 都导出成 ONNX再做 TensorRT 加速是近年“yolov7 部署”场景里最普遍的做法。这一节讲清楚整个链路并提示几个最容易忽略的坑。5.1 YOLOv7 导出 ONNX 的注意事项YOLOv7 官方仓库提供export.py可以导出 ONNX、TensorRT 等格式。但直接运行默认参数导出的模型可能带有NMS层导致模型结构不标准转 TensorRT 时反而变慢。我一般这样做python export.py \ --weights runs/train/exp/weights/best.pt \ --img-size 960 \ --batch-size 1 \ --simplify \ --include onnx \ --dynamic--simplify会用onnx-simplifier做图优化--dynamic允许动态宽高。注意如果不需要动态分辨率建议直接固定输入尺寸为 960×960TensorRT 的兼容性和速度都更好。导出后用onnxruntime验证一下输出确认输出张量是[batch, 25200, 85]这种原始预测结果而不是已经经过 NMS 的框。后续 NMS 逻辑在 TensorRT 之外用 Python 或 C 自己实现。5.2 CRNN 导出 ONNX 和 TensorRT 的序列维度CRNN 导出相对简单但要关注输入宽度的动态性。如果你的 CRNN 是固定 32×168 训练的直接导出 32×168 的静态图速度最快。但如果想支持各种车牌宽度就导出动态宽import torch dummy_input torch.randn(1, 3, 32, 168) torch.onnx.export( model, dummy_input, crnn.onnx, input_names[input], output_names[output], dynamic_axes{input: {3: width}, output: {1: seq_len}}, opset_version11, )导出时opset_version不要低于 11否则一些 RNN 算子如 LSTM在 ONNX Runtime 或 TensorRT 中会报“只支持旧格式”的兼容问题。TensorRT 对动态维度支持不如静态输入所以生产环境如果图片尺寸固定尽量用静态导出。5.3 TensorRT 加速时的精度与形状约束把两个 ONNX 转成 TensorRT engine一般用官方自带的trtexec或 Python API。以 TensorRT 8.5 为例一个直观的命令是trtexec --onnxyolov7.onnx \ --saveEngineyolov7.engine \ --fp16 \ --minShapesinput:1x3x960x960 \ --optShapesinput:8x3x960x960 \ --maxShapesinput:16x3x960x960注意--fp16模式YOLOv7 的检测框回归对 FP16 不敏感车牌识别 CRNN 的 CTC 输出对 FP16 也基本无损。但如果你在训练时用了图像归一化1/255.0到 TensorRT 里务必保持相同的预处理顺序比如先在 CPU 上除以 255再输入网络否则输出的置信度会整体偏移。--minShapes/optShapes/maxShapes这些动态维度参数要保证 batch 范围内每个输入的分辨率一致。当使用动态输入时TensorRT 的 Engine 会针对每个 shape 重新优化一次第一次推理较慢生产环境建议提前预热 1020 次。5.4 部署后验证与字符级调优部署完成后不要只看整体识别率。建议每张测试图输出“检测框坐标 置信度 识别字符串”的三元组按错误类型分类。常见的排查技巧如果识别的字符串第一位不是合法省份简称检查检测框是否切掉了车牌左边边缘通常是因为检测框太紧可以在裁剪时向左右各扩 5 个像素如果“渝 B12345”被识别成“豫 B12345”说明训练数据里这两个汉字的样本量不均衡需要针对性地增加该省份的图像如果新能源车牌中间多出一个小圆点分隔符CRNN 可能会把它识别成一个非法字符需要在字符集中将该位置设为 blank或后处理时直接过滤非字母数字和汉字的字符。下面的这张表是我在部署后做回归测试时用到的错误定位表现象可能原因调整方向检测框偏移 5~10 像素训练时标签框边缘太紧标注时外扩 2~3 像素识别结果少末尾字符宽度被压缩等比缩放 pad省份汉字混淆该省份样本少增强该省车牌图像蓝牌和绿牌错识别颜色通道预处理不一致检查 BGR/RGB 顺序检测漏掉远处车牌输入分辨率太低提高检测输入尺寸到 1280最后再说一个具体技巧在 TensorRT 部署 CRNN 时常见难点是 LSTM 的时间步并行程度不够导致 GPU 利用率上不去。可以把seq_len固定住比如 42并设置 TensorRT 的preview features里允许更激进的图优化。如果延迟还是高可以考虑把 CRNN 替换成基于 CNN 的 CTC 卷积网络如不含 RNN 的“全卷积 OCR”在车牌这种短序列任务上两者的精度几乎没有差别但推理速度能快 40%——这也是在边缘设备上值得尝试的方向。本文还有配套的精品资源点击获取
返回列表