拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

基于YOLOv8与CRNN的车牌检测识别及PyQt5界面实战

基于YOLOv8与CRNN的车牌检测识别及PyQt5界面实战 车牌检测与识别这个方向几乎是每个做计算机视觉的人都会碰一遍的入门实战题。原因不复杂它有明确的目标框、有清晰的字符语义、有落地的业务场景同时难度又刚好卡在纯分类太简单、通用检测太难的中间地带。我这里要聊的这套系统用的是 YOLOv8 做车牌定位配合字符识别模块完成整串号码还原外层套一个 PyQt5 桌面界面附带完整数据集、训练代码和推理源码。整套流程从数据采集一直跑到可以双击 exe 出结果中间踩过的坑不算少我把能复现的部分全部整理出来了。适合刚学完 YOLOv8 基础、想找一个完整项目练手的朋友也适合已经会训模型、但卡在怎么把模型变成能用的软件这一步的同学。下面不按教科书顺序讲就按我实际开发的推进顺序来。1. 项目整体架构与技术选型背后的取舍动手之前先想清楚一件事这套系统的边界在哪。车牌检测识别看起来是一个任务实际拆开至少包含三个子问题——车牌在哪里定位、车牌是斜的还是正的矫正、车牌上的字符是什么识别。很多新手一上来就想训一个端到端的大模型直接输出字符串结果发现标注成本翻了几倍精度还上不去。我最终选的是检测 矫正 识别三段式流水线每一段都独立可调、独立可替换出问题的时候能快速定位是哪一环拖了后腿。1.1 为什么拆成两段而不是端到端端到端方案听着诱人但车牌场景有几个现实约束让它不太划算。第一是标注成本要让模型直接输出字符序列你需要做序列标注或者字符级检测框标注一张图的工作量是单纯框一个车牌的三到五倍而我手里几千张图逐字符标完基本没时间做别的了。第二是可解释性两段式跑出来的中间结果是可视的检测框歪了能立刻看出来识别错了也能追溯是分割切错还是分类分错端到端只有一个黑盒输出调优全靠猜。第三是可维护性。车牌规格是会变的颜色、位数、字符集都可能调整。两段式里检测部分只管这是不是车牌识别部分只管上面是什么字换识别模型不影响检测换检测模型也不影响识别。这种解耦在真实项目里价值很高尤其是当你想把 YOLOv8 换成别的检测器做对比实验时几乎零成本。不过两段式也有代价中间环节多了误差会累积。检测框如果偏了或者漏了后面识别再准也白搭。所以我在设计时把检测的召回率放在第一位宁可多框几个候选也不能漏掉真车牌后面用简单的面积、长宽比过滤把误检剔除掉。1.2 YOLOv8 三个尺寸模型在实际场景的对比YOLOv8 官方提供了 n、s、m、l、x 五个尺寸我实际只在 n、s、m 三个里选因为车牌检测本身属于目标明确、特征强烈的任务用大模型属于浪费。我做了三组对照实验测试环境是 GTX 1660Ti 6G输入 640×640数据集约五千张模型参数量单帧推理耗时GPUmAP0.5模型体积YOLOv8n3.2M约 6ms0.972约 6MBYOLOv8s11.2M约 11ms0.981约 22MBYOLOv8m25.9M约 24ms0.983约 50MB数据摆在这就很清楚了s 相比 n 提升了约 0.9 个点代价是体积涨了三倍多、速度慢了一倍m 相比 s 几乎没提升纯属浪费。所以如果你的部署环境是普通显卡或者 CPU直接上 n如果对精度有执念、且显存够用 s。我最终交付版本默认用 n因为要在低端设备上也能跑起来同时留了 s 的权重让用户自己切换。这里顺带提一句轻量化的事。网上经常能看到仅 5MB 的目标检测模型这种说法其实 YOLOv8n 本身就差不多这个量级真正省体积的地方不在于换更小的架构而在于导出格式。用 ONNX 导出并做 FP16 量化体积能再砍一半速度还有提升后面部署章节会细讲。1.3 系统整体的数据流设计把整条链路画清楚对后面写代码帮助极大。我的设计是输入源图片/视频/摄像头→ 帧预处理缩放、归一化→ YOLOv8 检测车牌框 → 框裁剪并按置信度排序 → 透视矫正 → 字符识别 → 后处理规则校验 → 结果渲染回界面。每一步都有明确的输入输出契约我甚至把中间结果都做了可视化开关调试的时候能看到框在哪、矫正后长啥样、识别出的原始字符串是什么。这个习惯是从前辈那学的看起来多写了几十行代码实际上省下的调试时间远不止这些。尤其是字符识别死活不准的时候你把矫正后的图调出来一看发现是透视变换参数写反了五分钟就解决了。2. 数据集制作从采集到标注的完整链路模型精度上限由数据决定这句话在车牌任务里体现得特别明显。我见过太多人拿网上随便下的几百张图就开训训完发现模型只认白天、只认正面、只认蓝牌稍微换个角度或者晚上就全瞎。所以这一章我把数据从哪来、怎么标、怎么增强讲透。2.1 车牌框标注的规范与常见错误标注工具我用的是 labelImg格式选 YOLO txt。类别只有一类叫 plate不要自作聪明去区分蓝牌绿牌黄牌颜色信息交给后面的识别模块去判断更好。标注时有几个细节必须抠框的边界要贴合车牌的实际边缘包括车牌的外框边框。我见过有人只框住字符区域把上下白边排除在外这样训出来的模型在裁剪阶段会切掉部分字符识别直接崩。正确的做法是框住车牌整个矩形区域让边框完整包含。倾斜车牌怎么办不要试图用旋转框OBBYOLOv8 虽然支持 OBB 任务但会增加标注复杂度和训练难度。我的做法还是用水平外接矩形把倾斜车牌整个罩住然后在识别前做透视矫正。这样标注快、训练稳矫正环节用 OpenCV 十几行代码就能搞定。还有一个高频错误是同一张图里车牌被重复标注或者框的大小差异极大。前者会让模型学到矛盾的监督信号后者会让回归分支震荡。我的经验是标注完用脚本跑一遍检查统计一下所有框的长宽比分布正常车牌长宽比大概在 2.5 到 4.5 之间超出这个范围的框基本都有问题人工复核一遍能删掉不少脏数据。2.2 数据增强参数的实操选择YOLOv8 自带的数据增强很丰富但默认参数是给通用目标检测调的直接用在车牌上有几个坑。默认的 mosaic 概率是 1.0也就是每张图都做四图拼接这对车牌有风险——拼接后车牌可能被切到边缘甚至截断模型会学到半截车牌的特征。我的配置是把 mosaic 概率降到 0.5并且开启 close_mosaic在训练最后 10 个 epoch 关掉 mosaic让模型在真实分布上收敛一把。degrees 旋转我设成 5 度因为实际场景里车牌倾斜主要靠透视矫正处理不需要模型去硬扛大角度旋转。translate 设 0.1scale 设 0.5这两个是安全范围内的温和增强。色彩方面hsv_h 设 0.015hsv_s 设 0.7hsv_v 设 0.4。为什么饱和度和亮度放得比色相大因为车牌的关键信息在结构而不在颜色你可以把蓝牌染成偏青色、把亮度压暗模拟夜间但只要形状和字符还在模型就该认出来。反过来如果色相变化太大绿牌被调成蓝牌的样子反而会引入错误的监督信号。提示夜间和逆光样本一定要真实采集别指望靠亮度增强造出来。我试过纯靠增强模拟夜间模型在真实夜间图上召回率掉了将近二十个点后来补了七八百张真实夜间图问题立刻缓解。2.3 字符识别数据集怎么切分检测部分用 YOLO 格式就够了识别部分要单独准备。我用的是 CRNN CTC 的方案标注格式是图片路径 空格 车牌字符串每行一条。这里的核心难点在于中文字符。蓝牌第一位是省份简称共 31 个含港澳学警等特殊前缀按你的场景裁剪后面是字母和数字。绿牌是八位。这些都要在字典里定义好字典顺序必须和训练时一致否则推理出来的索引对应错字符结果全乱。切分字符的训练图我直接用检测框裁剪出来的原图不额外做精细的字符分割。因为 CTC 的好处就是不需要对齐模型自己学会在序列里找字符边界。这样做的好处是省事坏处是字间粘连或者有污渍时会识别成重复字符需要用后处理去重。数量上每个字符至少保证出现两百次以上稀有省份简称比如某些字母开头的特殊车牌如果样本太少我建议直接在字典里合并或者排除别硬撑。样本不均衡带来的长尾问题在字符分类里非常致命一个只出现十次的字符模型基本学不会。3. YOLOv8 训练全流程实战数据和结构都定了接下来就是训练。这部分我把它拆成环境、参数、监控三块每块都给出可以直接抄的配置和踩过的坑。3.1 环境搭建与版本踩坑环境这块我强烈建议单独建一个 conda 环境不要和系统 Python 混用。基础组合是 Python 3.8 到 3.10 之间挑一个我个人用 3.9兼容性最好。PyTorch 版本要和 CUDA 匹配如果你用 1660TiCUDA 11.8 加 torch 2.0 以上是稳妥的搭配。安装 ultralytics 直接 pip 装最新版即可但要注意它会顺带装一个特定版本的 opencv 和其他依赖如果系统里已经有旧版本可能出现 numpy 版本冲突导致 import 报错。我的解决办法是先装 torch确认 torch.cuda.is_available() 是 True再装 ultralytics这样能把依赖顺序理清楚。PyQt5 是另一个重灾区。PyQt5 和 PyQt5-tools 版本必须对应我见过因为版本错位导致 designer 打不开的情况。稳妥做法是 pip install PyQt55.15.9 PyQt5-tools5.15.9.3.3版本锁死别追新。另外在部分机器上PyQt5 界面会因为没有正确加载 OpenGL 而显示异常这个后面排查章节细说。3.2 训练参数配置与显存估算我用的训练命令大致是这样yolo taskdetect modetrain modelyolov8n.pt dataplate.yaml epochs100 imgsz640 batch16 workers4 device0其中 batch 是关键。1660Ti 只有 6G 显存batch16 加 imgsz640 基本能跑满如果报显存不足就把 batch 降到 8同时把 workers 也降一点因为 DataLoader 开太多进程也会吃内存。别一上来就 batch32除非你是 12G 以上的卡。学习率我不怎么动YOLOv8 默认用的是带 warmup 的自适应策略初学率 0.01 配合余弦退火对大多数数据集都够用。优化器默认 SGD如果你想收敛快一点可以换 AdamW但要注意改优化器时学习率也要相应调小一个量级。如果你想做迁移学习并且冻住一部分主干可以用 freeze 参数。freeze10 会冻结前 10 层适合数据量小、想快速出结果的场景。但车牌任务和 COCO 差异不小我建议要么不冻要么只冻前 5 层冻太多反而限制模型适应新任务。关于训练轮数我的经验是五千张图、单类别检测70 到 100 个 epoch 足够一般到 80 轮左右指标就平了。盯着 results.csv 里的 mAP50-95如果连续 15 轮没有提升就可以考虑停了YOLOv8 自带的 patience 参数设成 20 就能自动早停。3.3 损失曲线怎么看、什么时候停训练完第一件事是画损失曲线。YOLOv8 训练结束会在 runs 目录下生成 results.png包含 box_loss、cls_loss、dfl_loss 三条损失和对应的验证指标。怎么看这几条线是有讲究的。如果训练损失一直下降但验证损失开始抬头这是典型的过拟合说明模型在背训练集这时候要么加数据、要么加增强、要么提前停。如果训练损失震荡很厉害通常是学习率太大或者 batch 太小可以调小学习率或者开启梯度累积。如果三条损失都平得很早、指标上不去那多半是数据有问题比如标注错误或者类别太单一这时候该回头看数据而不是死磕参数。我还会额外画一张 mAP 曲线。box_loss 降只能说明框回归得准了但检测不只看框还看分类。真正决定模型能不能用的是 mAP50-95 这条线尤其是 0.5 到 0.95 这个严格区间的表现如果这条线只有 0.5 左右说明模型对框的定位还不够精细实际使用中裁剪出来的车牌会有偏移。我一般要求 0.5:0.95 至少到 0.8 以上才敢拿去用。4. 车牌矫正与字符识别实现检测框出来后直接扔给识别网络其实效果一般因为车牌可能是倾斜的、有透视畸变的。这一步矫正做得好识别准确率能拉高一大截。矫正完再做识别是整个流水线里技术含量最高的部分。4.1 透视矫正的坐标计算矫正的目标是把任意四边形的车牌变成标准矩形。思路是找到车牌的四个角点然后做透视变换。角点怎么找简单的方法是直接用检测框的四个角但检测框是水平矩形对倾斜车牌不准。好一点的方法是在检测框内做一次边缘检测或者颜色分割找到车牌区域的轮廓再用 cv2.minAreaRect 或者 approxPolyDP 取出四角。我用的是折中方案先用检测框裁剪出大致区域转灰度做一遍 Sobel 边缘检测再找最大轮廓用 approxPolyDP 拟合四边形。如果拟合出来的四个点长宽比在合理范围内就用它做变换否则退化回用检测框四角。这样既有鲁棒性又不会因为边缘检测失败而完全没法用。透视变换的映射关系是这样的把拟合出来的四个点按左上、右上、右下、左下的顺序排好映射到目标矩形的四个角。目标矩形的高度我固定设成 48 像素宽度按车牌原始长宽比乘以 48 得到一般蓝牌是 440×140比值约 3.14所以宽度大约 150。这个尺寸不要乱设要和后面识别网络的输入尺寸对齐否则又得多做一次缩放。def four_point_transform(image, pts): rect order_points(pts) (tl, tr, br, bl) rect width max(int(np.linalg.norm(br - bl)), int(np.linalg.norm(tr - tl))) height max(int(np.linalg.norm(tr - br)), int(np.linalg.norm(tl - bl))) dst np.array([[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]], dtypefloat32) M cv2.getPerspectiveTransform(rect, dst) return cv2.warpPerspective(image, M, (width, height))这段 order_points 的顺序一定要保证是顺时针且从左上开始如果顺序错了变换出来的图会翻转或者扭曲识别必然出错。我在这个函数上栽过一次调了半天识别精度最后发现是点的排序逻辑写反了。4.2 CRNN 识别网络的结构与训练要点识别网络我用的是经典的 CRNNCNN 提特征 RNN 建模序列 CTC 解码头。为什么不用纯 CNN 直接分类因为纯 CNN 需要先精确定位每个字符的位置再逐个分类遇到字符粘连或者边框有干扰就直接崩。CRNN 的 CTC 机制天然允许对齐模糊鲁棒性好很多。具体结构上CNN 部分我用了五层卷积每层后接池化和 ReLU输出一个高度为 1、宽度为 W/4 左右的特征图。RNN 部分用两层双向 LSTM隐藏维度 256。最后接一个全连接层映射到字符集大小加一多出来的是 CTC 的 blank。输入尺寸我固定成 32×160所以前面矫正时算出来的宽度要统一缩放。训练的核心参数是学习率和批次。CRNN 对学习率比较敏感我一开始用 0.01 直接发散后来降到 0.001 才稳定。batch 用 32 到 64 都可以看显存。损失函数用 CTCLoss注意设置 zero_infinityTrue否则遇到某些异常样本会报 inf。训练数据上我强烈建议加入一些真实的低质量样本——模糊的、有反光的、部分遮挡的。这些样本可以让模型学会从残缺信息里推断代价是训练前期损失降得慢一些但最终泛化能力明显更强。当时我加了两百多张夜间反光样本训练集准确率从 99% 掉到 97%一开始还挺慌但测试集上反而涨了三个点。4.3 后处理规则与车牌字典设计识别网络输出的原始字符串是没法直接用的必须做后处理校验。这里有几条规则我总结下来第一长度校验。蓝牌固定 7 位绿牌 8 位如果识别出来长度不对说明有丢字或者多字需要做修正。常见的修正是用 CTC 的置信度做过滤把置信度低于阈值我设 0.6的字符去掉再补位。第二字典约束。第一位必须是省份简称如果识别出个数字说明是错的可以从候选中选置信度最高的合法省份。第二到第七位是字母数字组合但要注意字母 I、O 容易和数字 1、0 混淆我在后处理里会把车牌中出现的 I、O 强制替换成 1、0因为车牌规范本来就不使用这两个字母。第三去重规则。CTC 在字符粘连时容易输出重复字符比如 京A12345 变成 京A112345这时候简单的连续去重不一定对因为真实的 京A11 这种也可能存在。我的经验是只在长度超限时才做去重而且要去掉的是置信度较低的那个重复字符。字典的构建要注意省份简称要按固定顺序排列字母数字也是。我的字典顺序是省份简称 数字 大写字母 特殊字符索引从 0 开始最后一个索引留给 CTC blank。这个顺序在训练和推理时必须严格一致换一次就要重新训一次否则识别全乱。5. PyQt5 界面开发与系统集成模型能跑了接下来要把它包装成一个能用的软件。这部分很多做算法的同学会忽略觉得界面不重要但实际上界面决定了你的项目能不能被别人使用也决定了演示时的观感。5.1 界面布局与交互逻辑设计我的界面分三块布局左侧是控制区包含模型加载、选择图片、选择视频、打开摄像头、开始识别这几个按钮中间是主显示区展示原图和识别结果右侧是结果列表记录每一次识别的车牌号和置信度支持导出 CSV。用 QMainWindow QDockWidget 的布局比纯 QWidget 灵活因为可以拖动面板也能适应不同分辨率。这里有个坑网上搜PyQt5 适配分辨率大部分方案是加缩放因子或者动态计算尺寸我实测下来最稳的还是设置固定的最小尺寸然后让布局管理器自己撑开不要手动去写死的坐标。字体和控件样式我会统一设置一遍 QSS用一套深色主题因为深度学习工具类软件深色看起来更专业也更护眼。控件大小上按钮高度设 36 到 40 像素用户点起来舒服太小了容易点错。5.2 多线程推理避免界面卡死这是新手最容易犯的错直接把推理代码写在按钮的点击回调里。单张图片还好一旦处理视频或者摄像头主线程被推理阻塞界面直接假死连关闭按钮都点不动。正确做法是用 QThread 或者 QThreadPool 把推理放到工作线程。我的实现是定义一个 Worker 类继承 QObject里面封装推理逻辑再用 moveToThread 挪到子线程。线程和主线程之间通过信号槽通信把识别结果发回主线程更新界面。class InferWorker(QObject): finished pyqtSignal(list) progress pyqtSignal(int) def run(self, source): results [] for idx, frame in enumerate(source): boxes model_detect(frame) texts recognize(frame, boxes) results.append((boxes, texts)) self.progress.emit(idx) self.finished.emit(results)记得在关闭窗口时把线程正确终止否则可能出现程序退不干净、后台进程残留的情况。我一般会在 closeEvent 里发一个信号让 worker 停止循环然后 wait 一下再退出。摄像头这块还有个细节OpenCV 的 VideoCapture 读取和 PyQt5 的显示必须解耦否则帧率会掉得很厉害。我的做法是单独开一个采集线程每读到一帧就 emit 出来显示主线程只负责画不负责读这样界面流畅度提升明显。5.3 打包与分发最后一步是打包成 exe。用 PyInstaller 一条命令就能打包但有几个坑必须提前避。第一YOLOv8 的权重文件要作为附加数据打包进去否则用户拿到 exe 找不到模型。第二torch 体积巨大打出来动辄几个 G如果只是演示可以考虑用 ONNX Runtime 推理体积能小很多速度也不差。打包命令大致是pyinstaller -F -w --add-data best.pt;. --add-data chars.txt;. main.py-F 是单文件模式-w 是去掉控制台窗口。单文件模式启动会慢一点因为它要先解压到临时目录如果在意启动速度可以用 -D 目录模式。打包后务必在一台干净的机器上跑一遍因为开发机上装了一堆库很多问题在开发机上根本暴露不出来。6. 常见问题排查与独家避坑指南这一章是我实际开发中踩过的坑整理成速查的形式遇到问题直接对号入座。6.1 环境与依赖类问题速查表现象可能原因解决方案import cv2 报 numpy 版本冲突numpy 版本过高或过低卸载重装 numpy 到 1.24 左右PyQt5 界面空白或黑屏OpenGL 未正确加载设置显卡驱动为最新或改用软件渲染torch.cuda.is_available() 为 FalseCUDA 与 torch 版本不匹配按官网对应表重装 torch训练时报显存不足batch 或输入尺寸过大batch 减半imgsz 降到 512PyQt5 install 特别慢网络或 pip 源问题换国内镜像源加超时参数关于 PyQt5 界面显示异常的问题我遇到过好几次表现是窗口能打开但是控件不显示或者整个窗口是黑的。排查下来基本都和环境里的图形渲染配置有关。稳妥的排查顺序是先确认显卡驱动是新的再确认 PyQt5 版本没有混用最后试试在代码里设置软件渲染的环境变量。这个问题在远程桌面场景下尤其容易触发如果你是在服务器上开发建议直接用本地环境跑界面。6.2 精度类问题定位思路识别不准的时候别急着改模型先按顺序排查。第一步看检测框是不是框歪了或者框偏了。第二步看矫正后的图是不是扭曲了或者被裁掉一部分。第三步看识别网络的输入是不是尺寸和训练时对不上。第四步才是模型本身的问题。我遇到过一次识别率莫名其妙掉了三十个点排查一圈发现是矫正后的图片宽度没做统一缩放导致 CRNN 的输入尺寸和训练时不一致。这种问题在代码里藏得很深只有在流程中间把图 dump 出来对比才能发现。字符混淆也是常见问题尤其是字母 D 和数字 0、字母 B 和数字 8、字母 S 和数字 5。这类混淆靠模型很难完全解决最有效的办法是后处理字典约束结合车牌规范做强制修正。比如国内普通车牌不出现字母 I 和 O那就直接把它们替换掉再比如某一位根据位置规则只能是数字那就从候选中挑数字。6.3 性能优化与部署经验GPU 上跑得飞快不代表 CPU 上能跑。如果你的部署环境是纯 CPU一定要提前做性能测试。我实测过YOLOv8n 在普通 CPU 上单帧要 80 到 150 毫秒勉强能跑 8 到 10 帧如果做视频实时识别会掉帧。这时候要么降输入分辨率要么用 ONNX Runtime 加量化加速。ONNX 导出是性价比最高的优化手段导出后配 ONNX Runtime 推理CPU 上速度能提升两到三倍模型体积也小一半。导出时注意 opset 版本要选支持你模型结构的太老会报不支持算子。量化用动态量化就够静态量化需要校准数据集麻烦一点但效果更好。如果要做多路视频同时识别建议开多个推理进程而不是多线程因为 Python 的 GIL 会限制多线程的并行效果。每个进程加载一份模型各跑各的这样才能真正吃满多核 CPU。注意模型文件在打包或部署时一定要校验一遍 MD5我就遇到过因为传输损坏导致模型加载报错排查了大半天才发现是文件本身坏了。最后分享一个我在实际项目里养成的小习惯每次训练完把权重、配置文件、训练日志、当时的 git commit 一起归档到一个带日期的文件夹里。刚开始觉得麻烦直到有一次换了台机器要复现半年前的实验结果翻出归档五分钟就跑起来了而旁边的同事还在群里问当时那个版本的数据集是哪份。这个习惯对个人项目尤其重要因为没人帮你维护版本记录。至于后续还能怎么扩展我目前在做的是把检测模型换成更轻的骨干同时加上车牌颜色分类分支一个模型同时出框和颜色省掉一次额外的判断逻辑等效果稳定了再来聊这块。
返回列表