ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

yolov5车牌识别系统实战:从环境配置到字符识别全流程

yolov5车牌识别系统实战:从环境配置到字符识别全流程 简介一份基于YOLOv5的深度学习车牌识别系统毕业设计论文面向计算机视觉、人工智能方向的毕业生和研究者系统阐述如何利用YOLOv5实时目标检测模型完成车牌定位、字符分割与识别全流程。论文内容覆盖图像采集与预处理、模型训练与调优、车牌精确定位、字符识别等关键技术环节同时详细展示了Python及OpenCV等工具在工程实现中的应用。资源文件为单个docx文档约1.14MB内含中英文摘要、目录、绪论及完整章节框架可直接作为毕业设计撰写和系统开发的参考蓝本。目前已有1006人学习下载适合需要快速搭建车牌识别系统或完成相关毕业设计的读者参考学习。1. 车牌识别没你想的那么难yolov5 深度学习系统拆开看停车场入口经常出现这种画面屏幕显示“未识别”栏杆纹丝不动后面喇叭催促。基于yolov5的深度学习车牌识别系统解决的正是这类实际场景——先让yolov5检测模型在画面里把车牌框出来再做矫正与字符识别最终输出一串完整车牌号。相比传统图像处理方案它对光照变化、车牌倾斜、运动模糊的容忍度明显更高这也是深度学习车牌识别成为毕设和工程落地主流方向的原因。这套方案适合正在做毕设、准备快速搭一套可用demo、或者想从数据到部署完整跑通识别链路的开发者。下面从环境配置开始一步步把这套系统搭起来。2. 先别急着训练yolov5环境配置与车牌数据集的三个准备步骤2.1 深度学习环境配置从零装到能跑detect.py环境配置在深度学习项目里是第一道坎很多教程喜欢把步骤写得很玄学其实yolov5官方依赖已经相当克制。常见做法是先用conda创建独立环境避免把系统自带的Python搞乱。Python版本选3.8最稳PyTorch选1.8以上都行我的建议是直接装最新稳定版。GPU版和CPU版二选一没有独立显卡就老老实实装CPU版或者后面选择租用服务器跑深度学习。# 创建独立环境避免污染系统Python conda create -n yolov5 -y python3.8 conda activate yolov5 # 安装GPU版PyTorch先确认显卡驱动版本再选对应的cuda版本 conda install pytorch torchvision torchaudio pytorch-cuda11.8 -c pytorch -c nvidia # 克隆yolov5源码并安装依赖 git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt这里有几个参数值得说明conda create -n yolov5里的yolov5只是环境名和源码目录名没有任何绑定关系两者同名纯属方便记忆pytorch-cuda11.8要对照你的显卡驱动版本来选驱动太低装了也调用不了GPU。装完之后先验证一下CUDA是否真的打通这一步能筛掉一半的环境问题python -c import torch; print(torch.version.cuda, torch.cuda.is_available())输出True说明GPU可用输出False说明PyTorch装成了CPU版或者驱动和CUDA版本不匹配。这时候别急着往后走训练一个识别模型要迭代几十轮CPU能把你熬到怀疑人生。我一般直接用nvidia-smi看驱动支持的CUDA版本再回头重新装对应版本的PyTorch比去网上一个个查兼容表格省时间。2.2 车牌数据集从哪里来公开数据集与自采数据的检查脚本环境跑通之后数据准备往往决定最终效果的80%。车牌识别任务的数据分布和通用目标检测不一样车牌在画面里占比小、宽高比固定、字符密集所以数据集质量比数量更重要。常用做法是取公开的中国城市车牌数据集CCPD做底子再往里面混入一部分自己拍摄的实际场景图。注意CCPD的标注是写在文件名里的不是标准的yolo格式直接拿过来训练前要先解析转换这个细节放在后面的避坑章里说。不管是公开数据还是自采数据喂给yolov5之前都要做一次体检。下面这个脚本我每次都会跑一遍它能一次性筛出三类问题缺少标注文件的图片、标注格式错误的行、标注框明显不合理的样本。import os import cv2 img_dir datasets/license/images label_dir datasets/license/labels min_size 20 # 车牌宽度低于20像素的框大概率是标注错误 for fname in os.listdir(img_dir): if not fname.endswith(.jpg): continue label_path os.path.join(label_dir, fname.replace(.jpg, .txt)) if not os.path.exists(label_path): print(f[无标注] {fname}) continue img cv2.imread(os.path.join(img_dir, fname)) h, w img.shape[:2] with open(label_path, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(f[格式异常] {fname}: {line}) continue cls, xc, yc, bw, bh parts bw, bh float(bw) * w, float(bh) * h if bw min_size or bh min_size or bw / bh 8: print(f[疑似错误标注] {fname} cls{cls} w{bw:.1f} h{bh:.1f})脚本逻辑不复杂但几个参数很关键min_size20是经验阈值正常车牌在1080p画面里宽度至少有几十像素小于20的基本是把远处的小车框进来了bw / bh 8是排查把整辆车框进来的情况标准车牌宽高比在3到4之间超过8就能认定标注有问题。yolo标注格式里存的是归一化后的中心点坐标和宽高所以读取时要乘回图片的实际宽高直接用原始值做判断会得出完全错误的结论。数据体检完还要按8:1:1分成训练、验证、测试三份。这里有一个容易漏掉的细节数据里至少要混入5%左右完全不包含车牌的背景图。原因是yolov5在训练时会在所有输入图上计算损失如果数据里全是带车牌的图模型会默认“画面里一定有个车牌”到了真实场景就会把类似纹理的区域误检出来。3. 跑通yolov5训练自己的数据集数据文件、训练命令与必调超参数3.1 数据配置与训练启动命令yolov5训练自己的数据集入口文件是data.yaml。这个文件告诉训练脚本数据在哪、有几类、类名是什么。车牌识别往往只检测“车牌”这一个目标所以配置文件非常简单path: datasets/license # 数据集根目录下面按train/val/test分 train: images/train # 训练图片目录 val: images/val # 验证图片目录 nc: 1 # 类别数只检测车牌一类 names: [license_plate] # 类别名称训练命令的写法相对固定核心参数就那几个python train.py \ --data datasets/license.yaml \ --weights yolov5s.pt \ --epochs 100 \ --batch-size 16 \ --imgsz 640 \ --device 0参数含义与选择逻辑--weights yolov5s.pt用的是官方预训练权重s版本参数量适中、在单卡上训练时间可控工程验证阶段没必要上来就开yolov5x--imgsz 640是速度和精度的默认平衡点车牌属于小目标显存够用的话开到1280会带来明显的mAP提升但推理延迟也会翻倍部署时反而得不偿失--batch-size 16在12GB显存下很宽裕显存不够就降到8或4不要硬撑。训练过程中的监控点有两条前20个epoch里train/loss是否在稳定下降每轮验证的mAP0.5是否在上升。如果loss在降但mAP一直趴着不动问题基本都在数据侧——要么标注错乱要么类别定义不一致这时候继续加大epochs只是浪费电。我可太多次在数据没检查好的情况下盲目调参最后全是白忙。3.2 yolov5超参数怎么调真正值得动的只有几个训练跑起来之后很多人喜欢去翻官方hyp.scratch.yaml里的超参数看到几十项就发怵。实际做yolov5超参数调优时真正值得动的只有几个。下面这张表给出我常用的参考区间超参数默认值建议范围作用与调整逻辑lr00.010.001 ~ 0.01初始学习率小数据集调到0.001防止早期震荡mosaic1.00.5 ~ 1.0是否启用马赛克增强数据量少时关掉防过拟合warmup_epochs3.03 ~ 5预热轮数让训练起点平稳weight_decay0.00050.0001 ~ 0.001权值衰减过大拖慢收敛小数据集取下限调超参数有一个血泪原则一次只动一个。很多人习惯把学习率、mosaic、anchor参数一排全改结果模型崩了完全不知道是哪个造成的。我一般先跑一个默认参数版本作为基准确认收敛后只改lr0再对比mAP变化批量改参不仅难以定位问题写出来的结论也没有说服力。另外两个容易翻车的参数值得单独说。第一个是mosaic官方默认开启它能把四张图拼成一张训练图增强模型对小目标的感知。但如果数据集本身只有几千张车牌图马赛克增强反而会把车牌切得支离破碎最后十轮还容易过拟合。第二个是adam和SGD的选择yolov5默认用SGD初学者换成Adam后往往发现前期收敛快了、后期精度上不去就把这口黑锅扣在模型头上——其实留默认优化器就行。如果本地显卡实在跑不动常见方案是租一台云GPU服务器按小时计费那种把数据集传上去训练完再下载权重。2000张左右的训练集用一张v100级别的卡一百轮大概一小时出头成本可控效果和本地基本没有差别。4. 从检测框到车牌字符串yolov5后处理与字符识别实现4.1 车牌区域提取与透视矫正yolov5检测模型输出的结果是一个个外接矩形框以及每个框的置信度。detect.py里的--conf-thres和--iou-thres两个参数直接决定输出质量前者控制置信度阈值默认0.25后者控制NMS去重的交并比阈值默认0.45。实际做车牌识别时conf-thres我会调到0.35以上因为车牌区域特征显著、检测难度低阈值低只会引入更多误检框增加后续识别环节的负担。检测框拿到之后直接裁剪出来的图像往往带透视变形——车停在坡道上、摄像头安装角度偏、车牌本身弯折都会让字符面目全非。这时候需要做透视矫正。常见做法是先取车牌区域内最大的边缘轮廓然后用approxPolyDP逼近成四边形最后做透视变换。import cv2 import numpy as np def rectify_plate(plate_bgr: np.ndarray) - np.ndarray: 对检测裁剪出的车牌图做透视矫正返回尽量端正的车牌图。 gray cv2.cvtColor(plate_bgr, cv2.COLOR_BGR2GRAY) edges cv2.Canny(gray, 50, 150) contours, _ cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return plate_bgr # 取面积最大的轮廓通常是车牌边框 contour sorted(contours, keycv2.contourArea, reverseTrue)[0] epsilon 0.02 * cv2.arcLength(contour, True) quad cv2.approxPolyDP(contour, epsilon, True) if len(quad) ! 4: return plate_bgr # 将四边形的四个角点按左上、右上、右下、左下排序 pts quad.reshape(4, 2).astype(np.float32) s pts.sum(axis1) diff np.diff(pts, axis1).ravel() tl pts[np.argmin(s)] br pts[np.argmax(s)] tr pts[np.argmin(diff)] bl pts[np.argmax(diff)] width int(max(np.linalg.norm(tr - tl), np.linalg.norm(br - bl))) height int(max(np.linalg.norm(bl - tl), np.linalg.norm(br - tr))) dst np.array([[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]], dtypenp.float32) M cv2.getPerspectiveTransform( np.array([tl, tr, br, bl], dtypenp.float32), dst) rectified cv2.warpPerspective(plate_bgr, M, (width, height)) return rectified参数说明集中在两处。epsilon 0.02 * cv2.arcLength是轮廓逼近的精度系数太小会把边缘噪声拟合成更多顶点太大则四边形变形明显0.02是多次试出来的稳妥值。角点排序的逻辑利用了矩形的几何特性左上角的xy最小、右下角最大、右上角的x-y最小这套判别在车牌轻微倾斜时仍然稳定。这个方案的前提是检测框里车牌区域占比较高且边框清晰。如果检测框留白太多轮廓提取会把无关区域包进去矫正结果反而更差——此时先按框裁剪再放大两倍做矫正效果立刻改善。要是车牌本身已经严重倾斜四边形逼近频繁失败就别硬扛了改用下一节的端到端识别方案更省事。4.2 字符分割与识别投影法实现与参数讨论矫正后的车牌图传统路线是分割出每一个字符再逐个识别。投影法是其中最经典、最好复现的做法先把图像二值化再做水平投影找出字符行的上下边界接着做垂直投影把每个字符的左右边界切出来。下面这套代码在干净车牌上的分割成功率很高代码量不大适合作为基线方案。import cv2 import numpy as np def segment_chars(plate_bgr: np.ndarray) - list: 分割车牌字符返回每个字符的裁剪图列表。 gray cv2.cvtColor(plate_bgr, cv2.COLOR_BGR2GRAY) # OTSU自动选阈值蓝底车牌会得到蓝底白字的效果 # 我们要的是字符为白色做一次反色 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) binary 255 - binary # 闭运算把字符笔画之间的断裂补上核形状用横向长条 binary cv2.morphologyEx(binary, cv2.MORPH_CLOSE, np.ones((3, 5), np.uint8)) h, w binary.shape # 水平投影统计每一行的白点数量找出字符行区域 row_sum binary.sum(axis1) / 255 row_idx np.where(row_sum w * 0.1)[0] if len(row_idx) 0: return [] top, bottom row_idx.min(), row_idx.max() # 垂直投影在行区域内统计每列白点数量 col_sum binary[top:bottom 1, :].sum(axis0) / 255 col_idx np.where(col_sum (bottom - top) * 0.1)[0] # 连续列聚合为字符块列间隔超过3个像素视为新字符 chars [] start col_idx[0] for i in range(1, len(col_idx)): if col_idx[i] - col_idx[i - 1] 3: chars.append((start, col_idx[i - 1])) start col_idx[i] chars.append((start, col_idx[-1])) # 按“车牌字符等宽”这一先验过滤铆钉和边框残留 char_w [c[1] - c[0] for c in chars] if not char_w: return [] median_w np.median(char_w) filtered [c for c in chars if median_w * 0.7 (c[1] - c[0]) median_w * 1.3] return [plate_bgr[top:bottom 1, c[0]:c[1] 1] for c in filtered]几个关键处必须解释清楚。第一为什么要先反色OTSU自动阈值只能区分前景和背景蓝底车牌的二值化结果是蓝底变白、白色字符变黑反色之后字符才是白色后续投影统计才有意义。第二MORPH_CLOSE的核为什么是(3, 5)而不是方形车牌字符笔画细密横向长条的核能把断裂补上、又不会把相邻两个字符粘在一起核再大就分不开了。第三垂直投影的阈值取(bottom - top) * 0.1行高十分之一的设定是为了滤掉细小的噪点列同时也避免把字符中笔画稀疏的列误判成空白。字符分割完成后每个字符裁剪图可以直接套一个简单的分类CNN类别就是省份简称、字母和数字的合集。这一步训练数据可以用开源字符集也可以从分割正确的样本里自动收集。投影法的局限在于对粘连字符束手无策——“苏”、“浙”这类笔画密集的汉字经常内部断开或被误切模糊图像里字符边缘又会粘连。更稳的做法是下一节的端到端识别直接把矫正后的车牌图映射成字符串。4.3 端到端识别LPRNet方案为什么更适合实际场景字符分割加分类的路线胜在每一步都可控、出了错好排查适合跑通基线。但实际做深度学习车牌识别系统更常见的方案是检测模型裁剪车牌后直接接一个轻量CNN做序列识别——主流的开源实现是LPRNet。它用CTC损失函数让模型自己学习字符之间的对齐关系不需要单独切割每个字符对倾斜、模糊、字符粘连的鲁棒性比投影法高一个档次。LPRNet的输入就是4.1矫正后的车牌图输出直接是车牌字符串。训练数据可以用检测阶段积累的真实车牌图再加一部分合成车牌——用程序把真实字符粘贴到纯色或带噪声的背景上可以几万张地生成把各种字体、倾斜角度、亮度变化都覆盖到。CTC训练不需要逐字符标注位置只需要整张车牌对应的字符串标注数据准备成本低很多这也是我向做毕设的人推荐这个方案的核心原因。5. yolov5车牌识别常见问题排查五条必踩的坑与解法环境、数据、训练、后处理整套流程走下来会遇到的问题其实高度集中。这些坑我基本都踩过一遍按“现象→原因→解决”的格式整理成五条对照排查能省下大量瞎试的时间。训练loss不下降mAP始终在零附近。这个现象最容易让人怀疑是模型代码坏了其实绝大多数情况是标签和数据集配置对不上。yolo训练要求每张图片对应的txt文件名与图片名完全一致且路径写在data.yaml里要正确。有一次我把train目录写成了val模型两个epoch就跑完一轮“训练”loss曲线也正常mAP却一直为零——因为val图片全部作为训练集的一部分参与了训练。解决方法是训练前跑一遍2.2节的数据体检脚本并逐个确认data.yaml路径和实际目录结构一致。这一条值得写进任何深度学习项目的checklist里。模型把车灯、车轮误检成车牌。现象很直观conf值还很高都在0.5以上说明模型确实学到了某种纹理特征。原因分两种一是训练集里车牌图片背景高度相似模型学到了背景而不是车牌本身二是负样本不足模型没见过“长得像车牌但不是车牌”的东西。解决的常用做法是收集误检图存成背景类负样本加入训练集再微调几个epoch。另一种快速缓解手段是调高conf-thres牺牲召回率换准确率但治标不治本。数据层面最彻底的做法是在训练集里混入大量没有车牌的车辆局部图、路面图、店铺招牌图类别数保持nc1不要新建负样本类——yolo把背景直接当背景处理。字符分割把铆钉、边框残留当成字符。这个坑出在投影法后处理里现象是分割结果多出一两个极窄的“字符”。原因是二值化图像里铆钉和边框在垂直投影上也有明显的白点堆积宽度接近单个字符的四分之一参与排序后干扰了字符宽度的统计。前面4.2节代码里我加了字符等宽的过滤条件——字符宽度落在中位数0.7到1.3倍区间内才保留这一道过滤在实际数据上能滤掉大半误分割。另一种有效手段是分割前用固定尺寸的腐蚀操作先把边框线去掉。注意腐蚀核大小要小于字符笔画宽度否则会把字符内部掏空这一步要针对你的图像分辨率试不要照搬参数。换了一个摄像头场景之后识别率断崖式下跌。训练时用的是公开数据集测试时用自己的手机拍了几张效果直接崩。原因很明确训练集和测试集的分布差异太大——摄像头角度、车牌光照、图像压缩程度全都不一样。这个问题的根源在数据侧解法也不复杂采集实际部署场景的几百张图加入训练集哪怕只是微调几十轮效果都非常明显。yolov5迁移学习的另一个技巧是固定前几层权重只微调后面几层用--freeze参数指定能在数据量有限时稳住已有特征提取能力。记住一个原则模型是数据的镜子场景差多少识别率就差多少。复制了公开超参数配置训练时报NaN或loss爆炸。常见做法是拿网上别人分享的hyp.yaml直接替换也不仔细看自己的数据量级结果没跑几轮loss就飞了。最典型的是lr00.01这个通用参数在几千张的小数据集上过大——数据量小每个batch的梯度方向波动大大学习率直接让loss冲上NaN。解决方式很简单小数据集把lr0降到0.001或打开--cos-lr让学习率按余弦曲线衰减能显著平缓前期震荡。遇到NaN还有一个容易被忽视的原因数据里存在纯黑或纯白图片归一化后输入方差为零优先排查训练目录里有没有损坏或异常图像这比调参数更快。6. 把实验结论做扎实用固定随机种子控制训练与数据划分训练跑通、识别正确之后紧接着要面对的问题是这个系统到底比别的方案好多少如果把结论写进论文或者汇报材料就需要一个能经得起推敲的实验方法。这里有一个非常朴素但极其关键的技巧固定随机种子让每一次实验都从同一起跑线出发。import random import numpy as np import torch def set_seed(seed: int 42): 固定Python、NumPy、PyTorch的随机种子。 必须在导入数据之前、任何随机操作之前调用 否则数据打乱顺序、模型权重初始化都会变化。 random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)random.seed控制Python内置随机数numpy.random.seed影响数据增强和数据集打乱的随机流程torch.manual_seed管模型权重的初始化。三个都固定两次训练的启动状态才能完全一致。调用时机也重要必须在数据加载器构建之前执行否则DataLoader里的shuffle顺序已经变了固定种子就失去了意义。这个技巧的实际价值体现在对比实验上。之前我做过一次模型结构的对比方案A比方案B高了2个百分点当时还兴奋了一下。后来发现只是数据划分的随机性在起作用——换了另一种数据划分方式B反超了A。自从固定种子并让两个模型使用同一份划分好的训练集和验证集结论才真正稳定下来。这个细节虽然简单却决定了实验结论的可信度也是很多深度学习项目做对比时容易忽略的一环。另外两个小习惯也很实用一个是训练日志的保存yolov5每次训练会自动生成runs/train/exp目录把每个版本的权重、超参数、曲线图都留在那里写总结时直接拿出来对比另一个是推理脚本里固定--iou-thres和--conf-thres否则同一套模型在不同阈值下测出的mAP会差好几个点这种低级失误在验收答辩时最容易被问住。把种子、阈值、数据划分都定死剩下的变量才真正属于模型本身。希望这篇笔记能帮你把车牌识别系统从头到尾搭起来少走几段我走过的弯路。本文还有配套的精品资源点击获取
返回列表