
简介面向毕业设计及深度学习视觉方向学习者这份项目基于YOLOv7目标检测与CRNN序列识别专门解决国内常见中文车牌蓝牌、黄牌、新能源等的检测与字符识别问题。压缩包共192个文件以Python源码为主体包含99个py脚本、yaml模型配置、训练好的pt/pth权重、jpg/png示例图片、shell部署脚本、Markdown说明文档以及C/CUDA推理文件整体仅55.64MB目录结构清晰便于直接运行与二次开发。资源提供完整工程代码、已训练模型、数据集及分步操作文档可快速跑通从车牌定位到中文文字识别的全流程也可作为课程设计或期末大作业直接提交。目前已有1276人学习下载适合正在准备毕设的学生和需要项目实战经验的CV入门者。在此基础上还可自行替换数据集或调整模型结构用于其他车牌样式或文字识别任务的拓展研究。1. 从停车场道闸到高速卡口YOLOv7CRNN 为什么是当前中文车牌识别的主流组合车牌识别这个需求远比想象中复杂。停车场道闸要看清蓝牌、绿牌高速卡口要抓拍时速 120 公里的黄牌货车工地出入口要分辨黄底黑字的大型车号牌甚至还要面对新能源绿牌的 8 位字符、港澳车双排车牌、污损和倾斜车牌。这些场景的项目源码大量基于 YOLOv7 做检测、CRNN 做识别两个模型串成两阶段流水线是当前工程落地里最常见也最稳的方案。两阶段拆开看YOLOv7 负责在一整张画面中定位车牌的四个角点并裁剪出车牌区域CRNN 负责把裁剪后的车牌图片转录成一串字符。这么做的好处是检测和识别解耦检测错了可以单独调检测识别错了可以单独换识别头不互相牵连。更重要的是CRNN 的结构天然适合不定长的序列输出中文车牌的省份简称、字母和数字混排长度在 7 到 8 位之间浮动恰好是 CTC 解码最擅长的场景。对刚入手这个方向的人这份源码和数据集的价值不只是能跑通而是能看清检测、识别、数据、部署这条完整链路每一步的取舍。2. 拆解 YOLOv7 检测与 CRNN 识别的技术分工2.1 YOLOv7 在车牌检测任务中的优势边界车牌检测本质上是小目标检测问题。在一个 1920x1080 的卡口画面里车牌往往只占 30x100 像素左右的区域算下来不到整图的 0.2%。YOLOv7 在原版 YOLO 系列基础上加入了 E-ELAN 结构和辅助训练头在不显著增加推理耗时的前提下提高了小目标的特征表达能力。它的 head 部分采用了重参数化卷积训练时是复杂的并行分支推理时fold成单路卷积这点和 YOLOv5 的尾段结构有本质区别也让它在 GPU 上跑 TensorRT 时更加友好。选择 YOLOv7 而不选 YOLOv8 或 YOLOv5 的原因在实际项目中往往是经验性的YOLOv7 的 Anchor 机制在处理极端宽高比车牌通常宽高比在 3:1 到 5:1时更易调整只需要改 cfg 里的 anchor 参数YOLOv8 虽然用 Anchor-Free 简化了流程但在这种固定形态目标上反而丢失了先验约束。这个说法不是绝对的但在车牌数据集上YOLOv7 的 mAP 和召回率表现通常是稳的。# 在训练前修改 yolov7/cfg/training/yolov7.yaml 中的 anchor 配置 # 默认 anchor 是针对 COCO 的 80 类通用目标车牌场景需要手动替换为小尺寸先验 anchors: - [5, 12, 8, 22, 12, 32] # P3 层负责小尺寸车牌 - [16, 40, 22, 60, 30, 80] # P4 层负责中尺寸车牌 - [40, 100, 60, 140, 80, 180] # P5 层负责大尺寸车牌这段配置对应 YOLOv7 的三个检测头每个检测头有 3 组宽高先验。车牌目标的宽高比普遍在 3 到 5 之间所以第一组 anchor 的 5x12 和 8x22 都是刻意拉成横向矩形而不是 COCO 默认的近正方形。如果你的数据集里包含倾斜视角导致的大宽高比车牌可以把最后一组改成类似 [45, 130, 55, 170, 70, 210] 这样的比例让模型更容易回归出细长框。2.2 CRNN 为什么适配中文车牌的不定长序列识别CRNN 全称 Convolutional Recurrent Neural Network核心是 CNN RNN CTC 三段式结构。车牌图片输入后先经过 CNN 骨干网络提取特征把图片变成一列特征向量然后经过双向 LSTM 建模序列上下文关系最后通过 CTC 损失函数对齐不定长的序列输出。车牌的字符集包含 31 个省份简称汉字、24 个大写字母除去 I 和 O和 10 个数字加起来约 65 类属于小字符集分类问题。关键点在于 CTC 机制。车牌图片的宽度经过 CNN 下采样后变成若干个时间步每个时间步对应一组字符概率分布但模型并不知道每个字符会占多少个时间步。CTC 通过引入空白符和去重合并规则让模型自己学会对齐字符序列和特征序列。中文车牌的字符数量是动态的——蓝牌 7 位绿牌 8 位警车、使馆车还可能是其他位数——CTC 天然支持这种变长输出不需要像传统方法那样先切分字符再逐个分类。CRNN 的训练还需要一个特殊的预处理把车牌图片统一缩放到固定高度和比例通常高 32 像素、宽按原图等比缩放后 pad 到 168 像素左右。在这么做之前数据加载部分要做下面这件事车牌检测框输出的是倾斜的四点框不直接是正矩形而 CRNN 期望的是水平文本行。这个预处理脚本本质上是做透视变换import cv2 import numpy as np def crop_plate_with_perspective(image, four_points): # four_points: [[x1,y1],[x2,y2],[x3,y3],[x4,y4]] 左上、右上、右下、左下 src np.array(four_points, dtypenp.float32) width int(max(np.linalg.norm(src[0]-src[1]), np.linalg.norm(src[2]-src[3]))) height int(max(np.linalg.norm(src[1]-src[2]), np.linalg.norm(src[3]-src[0]))) dst np.array([[0,0], [width-1,0], [width-1,height-1], [0,height-1]], dtypenp.float32) matrix cv2.getPerspectiveTransform(src, dst) return cv2.warpPerspective(image, matrix, (width, height)) # 推理时统一缩放先透视矫正再等比缩放到 H32, W 取动态值 # 最终 CRNN 输入尺寸为 (1, 3, 32, 动态W)配合 CTC 无需固定宽这里getPerspectiveTransform接收的是四个点坐标输出一个 3x3 的透视变换矩阵。车牌检测模型输出的四点框时序是不固定的有的模型输出 8 个值有的输出 4 个角点的归一化坐标要把它们还原成原图坐标后再做矫正。这一步如果漏了、或者用简单的仿射变换代替识别率会明显下降因为 CRNN 对字符的水平排列非常敏感。2.3 两阶段串联的工程编排方式检测和识别拼接起来最常见的编排方式有两种。一种是纯 Python 脚本按顺序执行YOLOv7 推理得到车牌框裁图后直接送进 CRNN。另一种是引入消息队列或 gRPC 服务做解耦比如检测服务部署在 GPU 机器上识别服务用 ONNX Runtime 部署在 CPU 上两者通过 Redis 队列通信。大多数项目源码采用第一种——简单直接延迟低适合单机场景。# 伪代码两阶段串联的核心逻辑 results yolo_model.detect(frame) # 返回所有车牌的 bbox 四点坐标 for plate_box in results: plate_img perspective_crop(frame, plate_box) # 透视矫正裁剪 plate_img resize_to_height(plate_img, 32) # 保持宽高比缩放到高度32 text crnn_model.recognize(plate_img) # 返回字符串如京A12345 print(f车牌: {text}, 置信度: {plate_box.conf:.2f})这段逻辑的细节在于resize_to_height不要直接cv2.resize到固定宽因为车牌字符间距和比例在固定宽度下会被压缩或拉伸CTC 识别对这种几何畸变很敏感。正确的做法是记录原始宽高比缩放到高 32 后宽度自然变化再在 batch 维度上做 padding 对齐。识别置信度这里打的是检测框的置信度实际更严谨的做法是整合 CRNN 的 CTC 解码置信度后面第 4 章会讲到。3. 数据集的标注规范与训练集构建策略3.1 车牌数据集的标注文件格式约定这份项目里的数据集标注格式通常有两种。如果检测部分用 YOLOv7那么标注文件是 txt 格式的 YOLO 标签每一行对应一个车牌框格式是class x_center y_center width height坐标都是除以图片宽高后的归一化值。如果检测部分训练前用了数据增强这部分代码还要负责在增强后同步更新坐标不能只增强图片不增强标注。# train/labels/IMG_0001.txt 内容示例 0 0.5234 0.3182 0.0821 0.0235 0 0.7612 0.4651 0.0934 0.0213第一列是类别 ID后面四列是归一化的中心点坐标和宽高。注意YOLO 格式的 bbox 是水平矩形如果用四点框标注倾斜车牌在训练前需要把四个点转换成一个外接水平矩形。这样做会引入背景噪声区域但 YOLOv7 的检测头本身预测的就是水平框后续 CRNN 的透视矫正需要的是四点信息因此在推理阶段往往还额外挂一个四点回归头或在仿射变换网络做角点修正开源项目里两种做法都有。识别部分的标注更直接每张车牌的图片文件名对应一个文本标签一般存成train.txt的纯文本列表每行是相对路径加标签字符串plates/001.jpg 京A12345 plates/002.jpg 苏B88888 plates/003.jpg 粤AD12345这里有一个容易忽略的细节省份简称字符集必须和训练数据严格对齐。如果数据集中只出现了 20 个省份的车牌那模型只能识别这 20 个省份遇到未出现的省份简称会输出错误结果。跑项目提供的源码时最好确认一下字符映射表char_dict.txt是否完整覆盖了 31 个省级行政区简称没有的话自己补齐汉字再重新训练识别头。3.2 中文车牌数据增强的 3 个必调参数车牌识别数据集的总量通常不大一份开源数据集可能几千张、几万张要覆盖全部省份、字体、环境和光照变化靠真实数据远远不够所以数据增强策略决定模型的上限。最常用的增强手段围绕三个参数展开直接影响模型的泛化能力参数建议范围说明调参依据亮度扰动 delta-50 ~ 50模拟逆光、夜间补光、阴影遮挡卡口照片的曝光变化比停车场大卡口场景取更大范围透视扰动幅度0.1 ~ 0.3模拟侧面抓拍、车道偏移视角超过 0.3 会导致字符过度形变识别几乎无法收敛模糊核大小1 ~ 3模拟运动模糊和低分辨率抓拍高速卡口车速快建议配合随机高斯噪声一起使用import cv2 import imgaug.augmenters as iaa import numpy as np seq iaa.Sequential([ iaa.AdditiveGaussianNoise(scale(0, 0.02*255)), # 高斯噪声模拟低照度 iaa.Multiply((0.7, 1.3)), # 亮度系数逆光暗/强光亮 iaa.Affine(shear(-5, 5), rotate(-3, 3)), # 小角度倾斜模拟安装角度偏差 iaa.GaussianBlur(sigma(0.0, 1.5)), # 运动模糊近似核不要太大 ]) def augment_plate(img): # 先做裁剪区域增强颜色类扰动再做几何增强 img seq(imageimg) return img实际训练时两个增强类要分开颜色类增强在识别模型中影响比检测大而几何类增强会把汉字的结构破坏掉。汉字识别比英文数字敏感得多一个“皖”字旋转超过 5 度人眼能认出来CRNN 可能就认不出来了这就是为什么 rotate 参数要控制在正负 3 度以内。3.3 合成数据补足真实数据的长尾分布车牌识别项目里有个很现实的问题真实采集的数据集里蓝牌数量远大于黄牌和绿牌省份分布也不均匀。最常见的解决方案是写一个车牌合成脚本用真实字体渲染一套车牌图像。中文字体要选黑体或宋体因为车牌的印刷字体是基于这两个体系略微变形得到的不能用系统自带的其他字体代替。from PIL import Image, ImageDraw, ImageFont def render_plate(province, alnum): # 生成 440x140 的蓝色车牌底图 plate Image.new(RGB, (440, 140), color(0, 50, 160)) draw ImageDraw.Draw(plate) # 第一个字符是省份简称字体略小且带分隔点 font_p ImageFont.truetype(simhei.ttf, 80) font_char ImageFont.truetype(simhei.ttf, 96) positions [(50, 20), (150, 10), (240, 10), (320, 10), (75, 15), (170, 10), (260, 10)] # 注意某些字符I、O不能出现在车牌中字符集构建时就要剔除 draw.text((50, 20), province, fontfont_p, fillwhite) for i, ch in enumerate(alnum): draw.text(positions[i], ch, fontfont_char, fillwhite) return platerender_plate生成的是基础车牌还需要配合 2.1 节的透视和光照增强一起用。要特别说明的是合成数据只解决字符分布的问题不能解决真实场景的背景噪声、遮挡、反光所以合成数据比例不要超过总数据量的三成否则模型对真实场景的泛化力会退化。把合成数据和真实数据混在一起训练时合成数据要参与前 200 个 epoch 的预热然后在最后 50 个 epoch 把合成数据占比逐步降为 0只用全真实数据微调收尾这个策略在多个车牌识别项目里都验证过效果。4. 模型训练YOLOv7 检测模型和 CRNN 识别模型的训练步骤4.1 YOLOv7 训练前的数据目录配置与超参数设置拿到这份源码第一步不是直接跑 train.py而是先检查数据目录的 YOLO 格式标准布局。数据集的 train 和 val 文件夹下各自要有 images 和 labels 两个子目录labels 里每个 txt 文件与 images 里的图片同名。目录配好了还需要一个 data yaml 文件指向这些路径YOLOv7 训练启动时就靠这个 yaml 文件确定数据位置和类别数量。# dataset/plate.yaml train: /data/plate/train/images val: /data/plate/val/images nc: 1 names: [license_plate]nc在这个项目里只有 1因为检测目标只有车牌一类。不要跟着 COCO 的 80 类习惯去写 80这会直接导致训练报维度错误。names里的类别名不会参与计算但会影响可视化标签的显示建议直接写license_plate不要用中文以免和某些可视化工具的编码冲突。# 训练命令示例在 yolov7 项目根目录下执行 python train.py --data dataset/plate.yaml \ --cfg cfg/training/yolov7.yaml \ --weights \ --batch-size 16 \ --img 640 \ --epochs 300 \ --hyp data/hyp.scratch.p5.yaml--batch-size取决于显存大小8GB 显存用 816GB 显存用 16batch 太小会导致 BN 层统计量不稳定训练后期出现震荡。--img 640表示训练时把输入图缩放为 640x640这个尺寸下既能保留车牌细节又不至于让训练速度太慢。注意推理时可以适当提高输入尺寸到 1280 来提升小目标召回率但训练时先开 640 让模型收敛稳定。4.2 YOLOv7 训练后的模型评估与剪枝策略训练完成后项目里一般会有best.pt和last.pt两个权重文件。best.pt是在验证集上 mAP 最高的权重last.pt是最后一个 epoch 的权重。先跑一次评估脚本确认关键指标python test.py --data dataset/plate.yaml \ --weights runs/train/exp/best.pt \ --img 640 \ --conf-thres 0.25 \ --iou-thres 0.5输出结果主要看三列Precision、Recall、mAP0.5。车牌检测场景下召回率比精确率更关键——漏检一个车牌意味着整条流水线拿不到数据而多检一个可以通过后续 CRNN 的置信度过滤掉。如果 mAP0.5 能达到 98% 以上可以尝试剪枝用 YOLOv7 自带的tools/prune.py对 BN 层 gamma 值做稀疏化训练然后裁剪掉不重要的通道。4.3 CRNN 识别模型的中文识别头设计与损失函数识别模型的训练重点是字符映射表和 CTC Loss 的配置。CNN 部分用 ResNet18 或 VGG16 作为骨干输出特征图高度下采样到 1宽度方向保留序列长度BiLSTM 隐藏层大小一般取 256双向后输出 512 维特征最后接一个全连接层输出维度等于字符集大小加 1CTC 的 blank 符。字符集构建顺序会影响模型输出的可解释性建议按“省份简称 字母 数字 blank”的顺序排列。# CRNN 的 CTC 解码关键逻辑去除重复字符和空白符 import torch def ctc_decode(output, char_list, blank_idx0): # output: (T, num_classes) 概率矩阵 _, preds output.max(dim1) preds preds.cpu().numpy() result [] prev -1 for p in preds: if p ! prev and p ! blank_idx: result.append(p) prev p # 注意CTC 的去重是先合并相邻重复再去空白符顺序不能反 return .join([char_list[i] for i in result])ctc_decode实现的是 CTC 的贪心解码逻辑是遍历每个时间步的输出把相邻重复的字符合并并跳过 blank 索引。这里最容易出错的地方是顺序先合并重复再跳过空白如果顺序反了“AA-”和“A-A”会被解码成不同的结果而 CTC 语义下它们是同一串字符。CTC Loss 在 PyTorch 里对应torch.nn.CTCLoss输入是(T, N, C)格式的 logits其中 T 是序列长度N 是 batchC 是字符类数。python train_crnn.py \ --train_file dataset/train.txt \ --val_file dataset/val.txt \ --char_dict dataset/char_dict.json \ --height 32 \ --width 168 \ --epochs 100 \ --batch_size 128 \ --lr 0.001--char_dict参数对应字符映射表训练前要检查 JSON 里的字符索引和预测时的映射表完全一致换了一个新模型权重但映射表对不上是识别结果全是乱码的第一大原因。--height 32 --width 168是固定输入尺寸实际宽高比在车牌的 3:1 到 4.5:1 之间168 宽对应约 4.5 的比例可以覆盖大部分车牌。4.4 训练中 GPU 显存不足的常见处理方式车牌图像的输入尺寸不大CRNN 的显存占用通常非常小真正吃显存的是 YOLOv7。如果出现 CUDA out of memory最常见的处理方式不是调小 batch而是改用梯度累积。显存不足报错发生在反向传播阶段梯度累积把一次大 batch 的更新拆成多次小 batch 的前向和反向每次反向只累加梯度不更新权重累计到设定步数后再执行 optimizer.step。python train.py --data dataset/plate.yaml \ --cfg cfg/training/yolov7.yaml \ --batch-size 4 \ --accumulate 4 \ --img 640--accumulate 4表示每 4 个小 batch 做一次参数更新效果等效于--batch-size 16。这不会减少总训练时间甚至因为 BN 层的统计量是在小 batch 上计算的精度还会略有波动但至少能让训练流程跑起来。更彻底的方案是在训练前用--img 416预训练 50 个 epoch然后加载预训练权重、恢复到--img 640再微调这样显存峰值被分摊到了两个阶段。5. 推理部署与精度调优把 YOLOv7CRNN 从跑通调到好用5.1 用 ONNX Runtime 做无 PyTorch 环境的快速推理项目源码里一般默认用 PyTorch 推理但实际部署到生产环境目标机器不一定装 PyTorch。最常见的做法是把 YOLOv7 的权重导出为 ONNX再用 ONNX Runtime 推理。YOLOv7 的仓库自带导出脚本关键选项是--grid和--dynamic车牌检测场景建议开启动态宽高来兼容不同分辨率的输入帧。python export.py --weights runs/train/exp/best.pt \ --img 640 \ --batch 1 \ --dynamic \ --simplify \ --grid--dynamic让 ONNX 的输入尺寸不固定这样单张 1080p 图片和批量小图都能进同一份模型--simplify做图优化去掉冗余算子--grid把推理结果直接解码成检测框省去外部 Anchor 解码步骤。导出的 ONNX 在 CPU 上跑一张 640x640 图大约需要 80 到 120 毫秒GPU 上跑 TensorRT 版本可以压缩到 10 毫秒以内。CRNN 导出 ONNX 时要额外注意序列长度动态轴。ONNX 默认把张量的所有维度都固定而 CRNN 的输入宽度是动态的导出时要显式指定 dynamic_axes 里的 width 维度import torch.onnx dummy_input torch.randn(1, 3, 32, 168) # NCHW torch.onnx.export( crnn_model, dummy_input, crnn.onnx, input_names[input], output_names[output], dynamic_axes{ input: {3: width}, # 第3维是宽允许动态变化 output: {0: seq_len} }, opset_version11 )5.2 中文车牌识别置信度过滤与错误纠正技巧模型输出的文本不能直接作为最终结果因为 CRNN 会有 OCR 常见的字错——比如“京”和“亰”字形相似模型可能张冠李戴。实用技巧是把 CRNN 的 softmax 概率输出落下来求整串字符的平均置信度然后再结合 YOLOv7 检测框的面积和时间连续性做一个综合判断。置信度区间处理策略场景建议 0.95直接入库作为最终结果写入数据库0.85 ~ 0.95抽查复核配合人审界面或者多帧投票 0.85丢弃或重试触发抓拍重试或降低道闸放行优先级多帧投票是车牌识别场景最简单有效的纠错手段车辆在画面中存在几十帧每一帧都做一次完整推理得到一串字符按字符串出现次数统计出现次数最多的结果作为最终输出。因为随机错误不会每次都在同一个字符位置发生而正确结果在所有帧里都是一致的投票后准确率会显著高于单帧结果。5.3 在 Jetson 或嵌入式设备上的推理优化优先级车牌识别的部署场景大量落在 Jetson Nano、Jetson Orin 这类嵌入式设备上这意味着推理优化不只是“把模型跑起来”还要在功耗和延迟之间取舍。优先级建议是先量化、再换后端、最后剪枝。量化优先用 FP16车牌检测对数值精度不敏感FP16 几乎不掉点INT8 量化虽然能把延迟再压一半但需要校准数据集校准数据的分布要尽可能贴近目标场景否则量化后的精度坑很难排查。最后一个容易被忽略的参数是检测帧的预处理分辨率。Jetson 设备上跑 640 输入和 1280 输入延迟差异接近 4 倍而车牌检测对分辨率的敏感度远低于通用目标检测——因为车牌本身是强纹理区域特征对比度高640 输入在 3 米距离内完全够用。只有在识别率持续偏低、且确认是模糊导致的情况下才有必要把输入提到 960 或 1280。判断的基线标准是在目标场景采集 100 张图跑一遍完整流水线统计每张图的单帧耗时和识别正确率用这个数据决定每一步的优化方向而不是凭经验猜测瓶颈所在。本文还有配套的精品资源点击获取