ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于YOLOv7的中文车牌识别:从环境搭建到模型微调部署

基于YOLOv7的中文车牌识别:从环境搭建到模型微调部署 简介面向计算机视觉与智能交通方向的学习者这份Python项目实现基于YOLOv7的车牌检测与中文车牌识别覆盖单双层车牌及蓝、黄、绿、白、武警、使馆、港澳等12种常见类型可满足毕业设计、课程设计、工程实训或企业原型演示需求。资源共97个文件以Python源码为主包含38个py脚本、yaml模型配置、pt/pth权重文件、C推理示例及测试图片文档说明与运行脚本齐备整体约24MB。已有193人浏览学习。压缩包内含完整检测识别流程从权重加载、图像预处理、双层车牌分割合并到中文文字识别均有对应实现目录区分模型导出、ONNX/TensorRT推理、Flask API等模块便于二次开发与算法对比研究。下载后建议先阅读README代码经多场景测试运行可靠适合在此基础上扩展其他车牌类型或部署到嵌入式平台。1. 基于yolov7的中文车牌识别项目到底解决了什么场景的痛点停车场出口的闸机、工地门口的道闸、路边临时检查点只要涉及车辆进出就绕不开“看清车牌”这个问题。市面上成品的车牌识别一体机确实多但一台上千甚至几千块的硬件成本加上厂商私有协议和云端依赖让很多个人开发者、小型项目组和安防集成商都觉得不划算。于是“用Python和yolov7自己搭一套车牌检测识别系统”就成了一个很实际的需求既能离线跑又能按自己的业务场景定制还不用担心被厂商锁定。这个项目就是围绕这个需求来的。它把任务拆成两个环节先用yolov7目标检测模型在画面里找到车牌的位置再把车牌区域裁出来交给识别模型读成字符串。相比传统的图像处理方法这套方案对角度倾斜、光照不均、复杂背景的容忍度高很多而且源码结构清晰文档齐备尤其针对中文车牌做了适配。所谓“支持12种中文车牌”指的是蓝牌、黄牌、新能源绿牌、警车白牌、军车、使馆黑牌等常见类型都覆盖在训练数据里不是只能识别普通家用车。对新手来说最友好的地方在于它不需要你从零训练一个模型源码里带了完整的训练、推理脚本和数据处理逻辑你只需要把环境配好、权重下载下来就能先跑通流程。对于有经验的同学这是一个可以直接二次开发的基础工程换数据集、调识别精度、导出部署都有现成的路径可以顺着走。下面我会按照“架构理解——环境跑通——模型微调——踩坑排错——部署优化”这条线把整个方案拆开讲清楚。2. 检测与识别串联架构yolov7在车牌任务中承担什么角色2.1 为什么选yolov7做车牌检测而不是yolov5或yolov8车牌检测本质上是一个单类目标检测任务输入是一张包含车辆的图片输出是车牌在画面中的矩形框。这类任务里yolo系列是落地最广的选项但你会在yolov5、yolov7、yolov8之间犹豫。我的选择逻辑很简单看它在同等精度下推理速度是否够快、工程生态是否成熟、二次改造是否方便。yolov7的特点是它在结构上引入了E-ELAN和辅助训练头在相同参数量下比yolov5的mAP更高推理速度却并不慢。对于车牌这种目标尺寸不大、画面中通常只有一个目标的场景yolov7的轻量级模型也能取得很好的效果。yolov8虽然新但是它的Anchor-Free结构在训练时需要更多数据才能收敛稳定而且车牌检测的公开权重和中文车牌相关经验更多还是集中在yolov5和yolov7上。从这个角度看yolov7是一个“性能有余、资料充足、改造成本低”的均衡选择。还有一个隐形的优势是部署生态。yolov7的仓库里原生带了导出ONNX和TensorRT的脚本这对后续把模型部署到Jetson、RK3588这类边缘设备非常关键。车牌识别场景经常是在闸机旁边放一台小主机普通GPU服务器不是常态能导出轻量模型直接决定了这个方案能不能真正用起来。2.2 车牌识别不是OCR而是“字符集受限的序列识别”检测拿到车牌框后下一步是把框里的图像转成字符串。很多人一上来就想到通用OCR引擎比如PaddleOCR或Tesseract。这其实是第一个认知偏差通用OCR面对的是任意文本中文车牌识别的字符集是固定的、有限的而且字符排列有强规律用通用OCR反而会因为字符集太广而产生误识别。车牌的字符集由以下三部分组成第一个位置是省份汉字常见的有京、津、沪、渝、冀、豫、云、辽、黑、湘、皖、鲁、新、苏、浙、赣、鄂、桂、甘、晋、蒙、陕、吉、闽、贵、粤、青、藏、川、宁、琼共31个左右第二位是发牌机关代号字母除去I和O后面是5位由字母和数字组成的序号。再加上新能源车牌比普通蓝牌多一位且第二位字符固定为D或F所以整个可选的字符集合只有70到80个字符。这比通用OCR动辄几千个常用汉字的规模小了一个数量级识别模型可以做得更小更快准确率也更高。这个项目里识别部分通常采用的是卷积神经网络加CTCConnectionist Temporal Classification的结构。具体来说输入车牌图像经过卷积网络提取特征然后按时间步输出字符序列的概率分布最后用CTC解码得到最终的字符序列。这种结构的好处是不需要对每个字符做精确的切分端点模糊的问题被CTC直接消化掉了训练时只需要图像和对应的完整车牌字符串标注。2.3 双层车牌的识别逻辑不是切两行图而是让模型适配两种版式双层车牌主要出现在大型货车和挂车上黄底黑字上排是省份字母下排是序号。很多项目在处理双层车牌时犯的错是把图片按水平中线硬切成两半分别识别后再拼接。这个做法在面对车身倾斜、车牌本身有弯曲时非常不稳定切出来的上下两行往往带有对方的残影识别准确率直接崩掉。常见的正确做法是让识别模型感知到“这张图是双层的”。具体实现有两种路径。第一种是把双层和单层作为分类标签在识别模型前面加一个轻量的版式分类头先判断是单层还是双层再走不同的识别分支。第二种更彻底——直接让序列模型适应双层排列把双层车牌视为一个行列结构用2D注意力机制或者把两行分别编码后合并解码但这会增加训练成本。对这个项目来说它选择的方案更接近第一种检测模型负责输出的还是一个整体车牌框识别阶段通过图像的高宽比和字符分布特征自动判断双层然后进行两行区域定位再分别走字符序列识别。单层蓝牌的高宽比大约是3:1双层黄牌大约是1.7:1左右这个比例是很好的先验特征。在实际解码时如果判断为双层车牌就按水平投影的字符间隙找到行分割线再对上下两行分别做序列识别最后将两行结果按上排加下排的顺序拼接成完整车牌字符串。这样处理比盲目整图识别要稳定得多也是这个项目在双层车牌上的核心设计。3. 跑通项目源码环境配置、推理脚本与完整执行顺序3.1 环境版本组合照着这个矩阵装能少折腾半天拿到“源代码文档说明”后第一件要做的事不是急着看模型结构而是先把Python环境固定下来。车牌识别项目虽然业务逻辑不复杂但yolov7的依赖和PyTorch版本之间的兼容性没有想象中那么平滑不少人在这一步就翻车了。下面这组版本组合是经过大量实践验证、出错率最低的依赖项推荐版本说明Python3.8 或 3.9不要用3.11以上部分依赖编译会出问题PyTorch1.10~1.13对应CUDA 11.3左右稳定且对yolov7友好CUDA11.3如果只是CPU跑可以跳过torchvision与PyTorch对应版本不要单独升级一定要配套安装opencv-python4.5.4.58 或 4.6.0.66新版本API变化不大但个别函数行为有差异numpy1.23.x版本过高可能导致opencv兼容告警创建虚拟环境是个好习惯。我的做法是conda create -n plate python3.8 conda activate plate pip install torch1.12.1 torchvision0.13.1 --extra-index-url https://download.pytorch.org/whl/cu113 pip install opencv-python4.6.0.66 numpy1.23.5 cd yolov7-Plate pip install -r requirements.txt第一行创建了干净的Python3.8环境第二行激活它第三行安装CUDA 11.3对应的PyTorch版本第四行固定opencv和numpy版本——这两个库版本不一致时最容易出现“图像读进来是None但不报错”的诡异现象。最后进入项目目录安装requirements.txt里的剩余依赖。装完之后别急着跑先验证一下PyTorch能不能调用GPU。这一步很关键因为yolov7的推理脚本会默认尝试用CUDA如果失败可能走CPU分支速度天差地别很多人以为代码有问题其实是GPU没启用。3.2 检测模型推理的最小命令与输出解读项目的源码里通常包含一个detect.py或detect_plate.py脚本这是整个系统里第一个要跑通的文件。它的作用只是加载yolov7权重对输入图片输出车牌位置框。先把这个跑通后续的识别和串接才有基础。最基本的执行命令是python detect.py --source ./test_images/ --weights weights/plate_detect.pt --conf-thres 0.4 --iou-thres 0.45 --img-size 640--source指定输入路径可以是单张图片、文件夹、视频文件或摄像头设备号--weights指向yolov7车牌检测权重文件一般是项目自带或需要单独下载--conf-thres是置信度阈值过滤置信度低于0.4的检测框调高会减少误检但可能漏掉模糊车牌--img-size是模型输入尺寸640是比较均衡的值压到320能提速但小目标容易丢失。跑完会在runs/detect/目录下生成带红框标注的图片。看输出时要关注两件事一是车牌框是否完整包住车牌四个角有没有裁掉边缘字符二是同一张图是否出现多个重叠框。如果多框说明置信度阈值偏低或者NMS参数不合适把conf-thres上调到0.5再跑一遍对比效果会明显变好。3.3 串联识别检测结果如何送入字符识别模型检测只是第一步真正的核心是识别。项目文档里一般会有一个较完整的推理脚本比如infer.py它的执行链条是读图→检测车牌框→裁出车牌区域→预处理→送入识别模型→输出字符串。我自己实现的串联逻辑大致如下# infer.py - 车牌检测与识别串联推理 import cv2 import torch import numpy as np def load_models(det_weights, rec_weights, devicecpu): # 加载yolov7检测模型 from models.experimental import attempt_load det_model attempt_load(det_weights, map_locationdevice) det_model.eval() # 加载车牌识别模型这里以常见的LPRNet结构为例 from lprnet import LPRNet rec_model LPRNet(class_num78, dropout_rate0.2) rec_model.load_state_dict(torch.load(rec_weights, map_locationdevice)) rec_model.eval() return det_model, rec_model def preprocess_plate(crop_img, height24, width94): # 识别模型输入尺寸固定保持车牌高宽比等比缩放到指定高度 h, w crop_img.shape[:2] scale height / h new_w int(round(w * scale)) resized cv2.resize(crop_img, (new_w, height), interpolationcv2.INTER_CUBIC) # 如果缩放后比目标宽度小右侧填充白边如果宽度超出则截断 if new_w width: pad np.full((height, width - new_w, 3), 255, dtypenp.uint8) resized np.concatenate([resized, pad], axis1) else: resized resized[:, :width] return resized def recognize_plate(crop_img, det_model, rec_model): processed preprocess_plate(crop_img) # 归一化并转成tensor维度为 [1, 3, 24, 94] tensor torch.from_numpy(processed.transpose((2, 0, 1))).float().unsqueeze(0) / 255.0 with torch.no_grad(): logits rec_model(tensor) # 输出 [seq_len, batch, num_classes] logits logits.permute(1, 0, 2).cpu().numpy() # CTC贪心解码每个时间步取最大概率的字符索引 seq [int(np.argmax(logits[0, t, :])) for t in range(logits.shape[1])] return decode_ctc(seq)代码里有两处值得细看。第一处是preprocess_plate车牌识别模型对输入尺寸有固定要求常见是24像素高、94像素宽。直接把任意尺寸的车牌图resize到这种形状会把字符压扁尤其是双层车牌的高宽比和单层差别很大必须按比例缩放再填充或截断。第二处是识别输出解码LPRNet的输出形状是[时间步, 批量, 字符类别数]取每个时间步最大概率对应的字符索引后还要做一个CTC去重操作——连续相同的字符只保留一个去掉空白符拼接成最终的车牌字符串。解码函数本身不长但边界情况不少def decode_ctc(seq, blank_index0): # CTC去重相同字符连续出现只保留第一个跳过blank标记 chars [] prev blank_index for idx in seq: if idx ! blank_index and idx ! prev: chars.append(idx) prev idx # 将索引映射为实际字符字符集顺序必须和训练时完全一致 mapping [京, 津, 沪, 渝, 冀, 豫, ...] # 与训练字符集保持一致 return .join([mapping[i] for i in chars])这里有个非常容易被忽略的坑字符集的索引顺序必须和训练时保持一致。如果训练代码里省份汉字按“京、津、沪、渝”排序你的推理代码也得按同样顺序建映射表否则识别结果看着像乱码。这个映射表在项目的data/plate_recognition_config.py之类文件里通常有定义直接用即可不要自己去改顺序。3.4 视频流与图片批处理的差异一个是对帧循环一个是一次性推理项目源码里很可能同时提供图片文件夹推理和视频文件推理的入口两者看似类似实际处理逻辑是有差异的。图片批处理时加载一张、推理一张、保存结果过程简单。但视频推理时你要考虑帧率控制、画面缓存和跳帧策略。处理视频流我一般直接用OpenCV的VideoCapture读流逐帧送入检测模型。性能较好的设备上可以做到全帧率处理设备性能一般的话典型的做法是每3帧取1帧做检测再结合目标追踪算法做结果平滑。这个项目如果文档里没有写跳帧逻辑你可以自己在主循环里加一个帧计数器不必为了每帧都跑检测而把设备配置拔高一大截。4. 微调训练自己的车牌模型数据集、标注格式与关键参数4.1 数据集从哪里来公开数据集与自采数据的组合策略如果需要识别的车型覆盖度足够高、省份足够全直接用项目自带权重可能不够尤其当目标场景是某个固定区域比如某个城市的工地、园区你会发现车牌地域分布极不均匀。这时候微调是必走的路。当前比较常用的公开车牌数据集是CCPDChinese City Parking Dataset它由中科大收集包含超过20万张车牌图像覆盖了蓝牌、黄牌、新能源绿牌并且标注信息很丰富。但CCPD的图片都是高清大图单张图片里车牌占比小直接用原始尺寸训练yolov7效率很低。常规做法是先用它的标注信息把车牌区域裁出来再组合成训练集。自采数据则要根据应用场景来定固定机位的摄像头画面是最有价值的训练素材。采集时注意覆盖不同时段的光照变化尤其是夜晚和逆光场景这两个条件下车牌检测的难度会显著上升。采集到的图片建议先做脱敏处理去掉无关的人和敏感信息后再进入标注流程。4.2 标注格式转换CCPD的JSON标注转成yolo的txt格式yolov7训练时用的标注格式是每张图片对应一个txt文件每行内容为类别ID 中心点x 中心点y 框宽 框高所有坐标都归一化到0~1之间。CCPD的标注是它的文件名里带的一串位置信息不是标准的VOC或COCO格式需要写个脚本转一下再喂给模型。CCPD文件名格式大概是025-95_91-214246-222273-200266-202300-202300-203300-22_0_0_30_...中间有一段冒号分隔的数字表示四个角点的坐标。转换脚本的核心逻辑如下# convert_ccpd_to_yolo.py - CCPD标注转yolo格式 import os import glob import numpy as np def parse_ccpd_filename(img_path): # 从文件名中提取车牌框的四角坐标 basename os.path.basename(img_path).split(.)[0] parts basename.split(-) # 第四段是四角坐标形如 214246_222273_200266_202300 corners parts[3].split(_) pts [] for corner in corners: x, y corner.split() pts.append([int(x), int(y)]) return np.array(pts, dtypenp.float32) def corners_to_yolo(corners, img_w, img_h): # 由四个角点计算最小外接矩形的中心点和宽高再归一化 xs corners[:, 0] ys corners[:, 1] cx (xs.min() xs.max()) / 2.0 / img_w cy (ys.min() ys.max()) / 2.0 / img_h w (xs.max() - xs.min()) / img_w h (ys.max() - ys.min()) / img_h return cx, cy, w, h # 遍历CCPD目录逐张转换 for img_path in glob.glob(CCPD2020/*.jpg): img cv2.imread(img_path) img_h, img_w img.shape[:2] corners parse_ccpd_filename(img_path) cx, cy, w, h corners_to_yolo(corners, img_w, img_h) # 每张图片对应生成同名txt文件标注内容只含车牌这一类所以类别ID为0 out_path img_path.replace(.jpg, .txt) with open(out_path, w) as f: f.write(f0 {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n)这个转换里最容易出问题的是角点顺序。CCPD的角点坐标顺序并不总是从左上开始有时候是乱序排列的直接取min和max计算外接矩形是安全的做法。别试图用四个角点去拟合旋转矩形yolov7输出的是轴对齐框旋转框标注反而会在训练时造成不必要的精度损失。4.3 训练配置与关键参数从预训练权重起步别从零开始车牌检测模型的训练推荐在yolov7官方预训练权重的基础上做迁移学习而不是从头训练。yolov7.pt在COCO数据集上的预训练权重虽然不认识“车牌”这个类别但它的底层特征提取能力已经很强了微调只需几十个epoch就能收敛从头训练则要跑到几百个epoch。训练命令一般长这样python train.py --data data/plate.yaml --weights yolov7.pt --epochs 150 --batch-size 16 --img-size 640 --device 0 --workers 4其中data/plate.yaml是数据配置文件内容指向训练集和验证集的路径。--epochs在数据量小几千张时150轮足够数据量大可以适当减少到80到100轮。--batch-size要看显存16对应8GB左右显存如果你的卡显存只有6GB改成8更稳妥。--workers是数据加载的线程数Windows上如果报DataLoader worker相关错误把这个参数改成0往往就好了。训练过程中的关键监控指标有两个一个是mAP0.5它表示IOU阈值0.5下的平均精度驾车牌检测通常要跑到0.95以上才算及格另一个是训练集和验证集的loss曲线是否同步下降如果训练loss下降但验证loss不降甚至上升说明过拟合了需要增加数据增强或减少epoch。4.4 字符识别模型的数据增强车牌字符的“肉眼可读”不等于“模型可学”检测模型之外识别模型同样需要微调且微调技巧和检测模型差异很大。车牌识别模型的输入是车牌区域图输出是字符串。想让模型对真实环境鲁棒需要做的不是简单地翻转和旋转——车牌字符是绝对不能水平翻转的一旦翻转字符的语义就变了比如“鄂”翻过来可能看着像另一个字。有效的增强手段我一般用下面这些增强方式参数建议说明高斯模糊kernel size 3~5模拟雨天和镜头失焦亮度对比度抖动0.6~1.4倍模拟白天到夜晚的光照变化透视变换轻微错切 ±5度模拟相机角度倾斜随机遮挡矩形遮挡小区域提升抗污渍能力噪声添加椒盐噪声密度0.01模拟图像压缩伪影增强要注意的是不要做得太过车牌字符本身结构紧凑过强的透视变换和遮挡会把字符结构破坏到人眼都认不出的程度模型也不可能学好。我的经验是增强后的样本至少要保证人眼能看出原来的车牌字符是什么这是一条硬底线。5. 避坑排查从漏检到编码乱码的5条现场记录5.1 GPU显存不足导致训练中断现象训练跑到第三个epoch终端直接抛出CUDA out of memory错误之前所有进度清空。原因这是最典型的训练配置问题通常是因为--batch-size设得过大或者--img-size用的640但显卡显存只有6GB中间特征图占满了显存。也有可能是开了太多--workers线程系统内存被占满后反而挤占显存。解决把batch-size从16降到8--img-size从640降到512。如果还要继续训练加上--cache-images参数把图片预处理结果缓存到内存可以显著降低训练时的显存占用。这是我几乎所有yolov7训练的默认操作别指望改某个隐藏参数能奇迹般省出显存最有效的永远是降batch和降分辨率。5.2 检测框贴边导致识别乱码现象检测模型画出的车牌框紧紧贴着字符边缘裁出来的图里“京A12345”的首尾字符缺失一半识别输出变成了缺字或乱码。原因训练标注时车牌框标注得过紧模型学到了“框要贴着字符”的错误模式。车牌本身是有物理边框的标注框应该包含车牌边框到字符之间的那一小段间隔区域这个间隔虽然有颜色和背景干扰但对识别阶段的上下文理解有帮助。解决有两种做法。一种是检查数据集标注所有标注框向外扩展4到6个像素再重新训练。另一种更省事推理阶段在裁切车牌框时直接做扩展x_min max(0, x_min - int(w * 0.02))这样的方式把裁出来的图片两侧各多留2%的背景区域。我实测后者对识别准确率的提升比前者更直接。5.3 双层车牌被识别成单层乱码现象黄牌货车经过时检测框定位正常但识别模型输出的字符串完全是乱的和车牌实际内容毫无对应关系。原因识别模型处理双层车牌时如果按单层逻辑去解码整张长条图模型会尝试用序列方式读“两行”内容输出的字符顺序和实际排列对不上。本质是识别模型没有被训练过处理双层版式或者推理时没有触发双层判断分支。解决推理代码里先用高宽比做一次预判长宽比小于2.2时按双层逻辑处理先按水平投影找到两行的分割坐标再分别送入单行识别流程。这个预判不需要额外推理开销只是加一个if分支。注意分割点不要取中间位置应该把下半部分的行间隙剪出来——双层车牌的上排通常比下排窄且上下高度一般不相等。5.4 省份汉字识别混淆皖、豫、冀分不清现象其他字符都识别正确唯独省份汉字出错“皖”经常被识别成“豫”或“冀”明明字长得很不一样。原因这是典型的样本不均衡问题。训练数据以蓝牌为主蓝牌里安徽、河南、河北的车牌数量分布不均如果某些省份出现次数极少模型对对应的汉字就不敏感。此外省份汉字笔画相对密集在低分辨率下容易丢失细节。解决对识别数据做字符级加权采样。统计训练集中每个省份出现的次数对出现次数低于平均值的省份进行图像过采样复制若干次并入训练集。另一种做法是单独收集这几个易混省份的车牌图片专门补充50到100张就能见效不需要追求数量级的大规模采集。5.5 Windows下训练脚本运行报编码错误现象在Windows上执行训练脚本报错UnicodeDecodeError: gbk codec cant decode byte定位到是读取txt标注文件时崩溃。原因Windows默认的中文编码是GBK而yolov7的标注读取模块用的是UTF-8编码读取文件。如果某个标注文件里包含了中文字符或者路径本身带中文就会触发这个解码错误。这个和车牌内容无关是纯环境问题。解决在训练脚本入口处加两行环境变量声明import os os.environ[PYTHONIOENCODING] utf-8再把项目代码里读取文件的地方统一加上encodingutf-8参数。如果是pandas或csv读取出现同类问题在读取参数里指定encodingutf-8-sig可以兼容带BOM的文件头。6. 把模型压到生产可用ONNX导出、视频流降载与结果平滑在生产环境部署yolov7车牌检测模型时我一般会把模型导出成ONNX格式再推理而不是直接用PyTorch的GPU图模式。这么做有两点考量一是ONNX不依赖PyTorch的版本环境只要是同一份ONNX文件在服务端和边缘设备上行为一致二是ONNX模式下的推理延迟比PyTorch的eager模式更稳定可控。导出命令并不复杂在yolov7仓库的export.py脚本下执行python export.py --weights weights/plate_detect.pt --grid --simplify --img-size 640 640 --batch-size 1导出后用ONNX Runtime加载推理接口非常简单但要注意输入输出的维度格式是[N,C,H,W]颜色通道是RGB还是BGR要和训练时保持一致很多人在这一步没有对齐结果推理出来的检测框位置完全对不上。生产部署另一个不容忽视的问题是视频流处理的速度策略。检测模型本身在GPU上每帧只花十几毫秒但加上图像读取、缩放、归一化、识别的整条链路后实时性会打折扣。我的常规做法是引入跳帧和结果队列平滑机制每2帧检测一次识别结果用一个长度为5的滑动窗口缓存窗口内出现频率最高的字符串作为当前结果输出。这样可以过滤掉偶然的识别抖动又不会因为连续几帧误识别造成显示混乱。最后说一个我自己的血泪经验车牌识别系统的准确率瓶颈十个里有八个不在模型结构而在拍摄角度和打光。项目源码调得再好实际部署时摄像头只要离地面太高、俯角太大模型能力再强也白搭。我现在的习惯是部署前先用测试集里的真实场景图试跑一遍看看哪些照片识别失败再对着失败样本去调机位。希望帮到你。本文还有配套的精品资源点击获取
返回列表