YoloV5口罩识别实战:从数据集准备到GUI部署全流程解析
2026/9/15 2:33:14 网站建设 项目流程

简介:基于YoloV5的口罩识别模型项目,完整包含GUI交互界面、Python源码、模型权重与详细开发文档,适合人工智能、通信、自动化、电子信息等专业学生用于毕业设计、课程设计或项目演示,也便于初学者从数据配置到模型推理完整走通流程。压缩包共1650个文件,以779个Python脚本和778个pyc编译文件为主,搭配yaml模型配置、pt权重文件、exe可执行程序及txt说明文档,能够覆盖环境构建、模型训练、界面运行和后端依赖等关键环节,整体约139.38MB,目录结构清晰,便于快速定位与二次开发。项目代码已经过导师指导认可并测试通过,答辩评审分达95分,可直接运行使用,也可在此基础上扩展功能或替换数据集实现其他目标检测场景。目前已有95人浏览学习,适合需要高质量参考源码与完整项目资料的人群下载使用。

1. 口罩识别为什么都选YoloV5:从基线到项目化

做口罩识别项目,最怕的不是模型结构不熟,而是数据、训练、界面三个环节互相脱节。YoloV5之所以在毕设、课程设计和企业DEMO里被反复使用,是因为它把目标检测的标准流程压缩得很干净:准备好标注数据集,改一个yaml,跑train.py就能得到可用的权重。这套口罩识别资源的价值在于,它不止给你训练脚本,还带着GUI源码和详细文档,覆盖从yolov5环境配置到最终可视化展示的完整链路。适合两类人:一类是准备用yolov5训练自己的数据集的在校学生,另一类是工作中需要快速验证检测效果的工程师。用同一条基线,后续换水果识别、车牌识别的数据集,也能复用大部分工程代码。

2. YoloV5环境配置与口罩数据集准备

2.1 虚拟环境与依赖版本:别在torch上翻车

压缩包里自带了一套venv虚拟环境文件:activate.bat、deactivate.bat、pyvenv.cfg、python.exe等,说明作者在交付时把运行环境一起打包了。这个做法很常见,但我不建议直接双击activate.bat使用,因为pyvenv.cfg里写死了Python解释器的home路径,换一台机器后路径失效,环境会直接报错。更稳妥的做法是自己重建一个干净的venv,再把依赖装进去。

# 创建虚拟环境 python -m venv venv # Windows下激活 venv\Scripts\activate.bat # Linux/macOS下激活 source venv/bin/activate # 安装依赖 pip install -r requirements.txt

这里python -m venv会生成和你压缩包中类似的文件结构,包括activate.bat、pyvenv.cfg和python.exe。requirements.txt需要从YoloV5官方仓库拉取当前release对应的版本,不要手工混合安装torch和torchvision,否则运行train.py时会出现libcudart和cudnn不匹配的报错。我一般会先执行python -c "import torch; print(torch.cuda.is_available())",确认GPU可用后再装requirements。

如果你的机器没有N卡,CPU也能训练,只是速度慢。口罩数据集只有几千张的话,用CPU跑yolov5s可以把轮数减到60轮。推理阶段CPU完全够用,ONNX Runtime优化后单帧200毫秒左右。环境配置常见的报错和对应处理方式,我整理成了一张表:

报错信息原因处理方式
No module named 'torch'先装了其他依赖,torch没有装单独安装torch后重装requirements
cuDNN errortorchvision与torch版本不匹配对照官方requirements重新安装
Label class x exceeds nctxt标签中的class_id超过类别数检查mask.yaml的names顺序与标签是否一致

这三个问题在yolov5环境配置里出现频率最高。尤其是第三个,我见过有人把with_mask标成1、without_mask标成0,训练前忘了改mask.yaml,结果labels里的class_id和names对不上,训练过程不报错但mAP一直很低。

2.2 口罩数据集的三种来源与标注格式转换

口罩识别数据集通常有三个来源:公开的口罩检测数据集,例如AIZOO和Mask Wearing Dataset;自己拍摄或爬取图片后用LabelImg标注;以及从监控视频抽帧做半自动标注。第一种最省事,但注意原数据集的类别顺序可能不是我们想要的。如果原始标注是with_mask和without_mask,而mask.yaml里names顺序是['face','mask'],就需要先转换标签。

YoloV5使用的标注格式与VOC的XML不同。每张图片对应一个同名txt文件,每一行表示一个目标,格式为class_id center_x center_y width height,坐标全部归一化到0到1。下面这段代码可以把VOC XML批量转为YOLO txt:

import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, out_dir, class_names): # 解析XML文件 tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') w_img = int(size.find('width').text) h_img = int(size.find('height').text) lines = [] for obj in root.iter('object'): cls_name = obj.find('name').text if cls_name not in class_names: continue cls_id = class_names.index(cls_name) bbox = obj.find('bndbox') xmin = int(bbox.find('xmin').text) ymin = int(bbox.find('ymin').text) xmax = int(bbox.find('xmax').text) ymax = int(bbox.find('ymax').text) cx = (xmin + xmax) / 2 / w_img cy = (ymin + ymax) / 2 / h_img w = (xmax - xmin) / w_img h = (ymax - ymin) / h_img lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") out_name = os.path.splitext(os.path.basename(xml_path))[0] + '.txt' with open(os.path.join(out_dir, out_name), 'w') as f: f.write('\n'.join(lines)) class_names = ['face', 'mask'] # 顺序必须与mask.yaml中的names一致 voc_to_yolo('sample.xml', 'labels', class_names)

这里有几个容易出错的地方。一是xmin等坐标必须先转int再计算,避免字符串拼接错误;二是class_names的顺序决定了class_id,一旦开始训练就不能改,否则之前生成的txt全部要重新生成;三是w和h保留6位小数就够,越多只是增加文件体积。YoloV5官方train.py启动时会自动扫描labels目录,如果发现某张图片没有对应txt,会在日志里打印missing label,可以根据这个信息排查转换遗漏。

数据准备好后通常按9比1划分train和val。我用下面这段脚本把图片和标签同时移动:

import os, random, shutil random.seed(42) data_root = 'mask-dataset' images = [f for f in os.listdir(os.path.join(data_root, 'images')) if f.endswith('.jpg')] random.shuffle(images) split_idx = int(len(images) * 0.9) for split, imgs in [('train', images[:split_idx]), ('val', images[split_idx:])]: os.makedirs(os.path.join(data_root, 'images', split), exist_ok=True) os.makedirs(os.path.join(data_root, 'labels', split), exist_ok=True) for img in imgs: label = img.replace('.jpg', '.txt') shutil.move(os.path.join(data_root, 'images', img), os.path.join(data_root, 'images', split, img)) shutil.move(os.path.join(data_root, 'labels', label), os.path.join(data_root, 'labels', split, label))

固定random.seed(42)保证多次执行结果一致,建议在项目文档里记下这个随机种子。实际使用中,如果样本量很小,比如每个类别只有两三百张,9比1会让验证集数量太少,mAP波动大,我一般会改成8比2,同时打开--cache让验证集加载稳定一些。

2.3 数据集配置文件mask.yaml

完成目录结构后,还需要写mask.yaml。这是yolov5训练自己的数据集时最容易被忽略的文件。我把常用配置写在下面:

train: mask-dataset/images/train val: mask-dataset/images/val nc: 2 names: ['face', 'mask']

train和val建议写绝对路径。YoloV5虽然也支持相对路径,但相对路径基于执行train.py的工作目录。在PyCharm里点击Run,工作目录可能是项目根目录;在命令行里可能是venv所在目录,路径不一致会直接报File not found。nc是类别数,names的顺序严格对应标注文件中的class_id。class_id=0对应未戴口罩的人脸,class_id=1对应戴口罩的人脸。我习惯把未戴口罩放在前面,这样混淆矩阵第一行就是漏检最多的类别,比较方便观察。mask.yaml建议放到项目根目录或data目录下,不要在压缩包里嵌套太深。

3. 模型训练与超参数调优

3.1 训练命令与关键参数表

在数据准备完成之后,直接运行官方train.py即可开始训练。对于口罩识别这种小目标场景,不建议一上来就用yolov5l或yolov5x,模型过深反而容易在小数据集上过拟合。我通常用yolov5s起步,跑通后再尝试yolov5m。命令如下:

# 使用yolov5s做迁移学习,数据配置为mask.yaml python train.py --data mask.yaml --cfg yolov5s.yaml --weights yolov5s.pt --epochs 100 --batch-size 16 --img 640 --device 0 --cache

对照这个命令,关键参数含义如下表:

参数作用口罩识别建议
--data数据集配置文件mask.yaml
--cfg模型结构配置yolov5s.yaml或yolov5m.yaml
--weights预训练权重yolov5s.pt,从官方release下载
--epochs训练轮数100起步,过拟合再降到60
--batch-size批量大小显存8G以下用8,16G用32
--img输入分辨率640起步,小目标可提高到768
--cache数据缓存显存够用选true,否则用--cache disk

第一次训练时,YoloV5会从COCO预训练权重加载特征提取层,同时把分类头输出维度从80改成nc=2。新层的权重是随机初始化的,所以即使只有几百张口罩图片,模型也能收敛。原因是前面的骨干网络已经学会了通用特征,需要重新训练的只是后面的分类和回归头。

另外,--img不一定要用640。口罩在街景图片中占比通常比较小,把输入分辨率从640提高到768,对小目标的召回率大约能提升1到2个点。代价是训练时间和显存占用上升。显存不够时可以用--rect减少输入区域的空白部分,但会改变batch内图片的分布,新手不建议开。

3.2 超参数调优:lrf、mosaic和fliplr

YoloV5把学习率衰减、数据增强参数放在hyp.scratch-low.yaml里。默认参数在COCO上有效,直接搬到口罩识别上不一定最优。我自己实验时这样调整过,mAP@0.5从0.85拉到了0.92,关键修改如下:

lr0: 0.01 # 初始学习率 lrf: 0.2 # 最终学习率 = lr0 * lrf momentum: 0.937 warmup_epochs: 3.0 mosaic: 1.0 # 是否启用马赛克增强 mixup: 0.2 # mixup强度 fliplr: 0.5 # 水平翻转概率 scale: 0.5 # 随机缩放范围

为什么要动这几个参数?lrf从默认的0.01改成0.2,学习率在最后一轮衰减到初始值的20%。对于小数据集,学习率降得太低会在后期陷入局部最优,pr曲线上precision很高但recall偏低。mosaic默认是1.0,每张训练图都做四图拼接。但口罩数据集中很多是单人近景,mosaic把四张图缩到一张,目标变得更小,反而不利于小目标学习。我把mosaic降到0.5,训练稳定性变好,loss曲线不再出现周期性尖峰。mixup加了0.2,能让模型更关注前景和背景的区分,减少口罩误检。

注意超参数不是越大越好。fliplr对左右对称的物体很安全,但如果你把口罩戴在左耳和右耳看作不同语义,那水平翻转会制造标签噪声。口罩是对称目标,所以0.5是安全的。scale调低到0.5后,目标在训练过程中不会被缩放得太小,适合街拍图。这些调整不需要重新从零训练,可以在原权重基础上用--weights best.pt继续训练,我一般用lr0=0.001再跑20轮。

3.3 验证与观察曲线

训练完后,YoloV5会在runs/train/exp/下生成结果。不要只盯着最后的mAP,先打开results.png,看train/obj_loss和val/obj_loss两条曲线是否同步下降。如果val_loss在某个epoch后开始上升,说明过拟合,应该提前停止;如果train_loss下降但val_loss不动,大概率是数据集标注有问题,需要回去检查labels目录下的txt是否有目标超出图像边界。

用验证集重新评估一份权重:

# 在验证集上评估best.pt python val.py --data mask.yaml --weights runs/train/exp/weights/best.pt --img 640 --task val

val.py会打印每个类别的precision、recall、mAP50和mAP50-95。对于口罩识别,我更关注recall而不是precision,因为漏检一个未戴口罩的人比误检一个戴口罩的人更严重。如果recall低,优先降低conf_thres;如果precision低,优先检查是否存在大量漏标目标,漏标会让模型学到错误的背景特征。

4. GUI源码解析:用PySide6把YoloV5推理封装成桌面应用

4.1 为什么选PySide6而不是Tkinter

训练好模型后,如果要给答辩评委演示,命令行下跑python detect.py --source 0虽然能跑,但观感很差。GUI的价值就是把模型的推理输出绑定到用户操作上。用Tkinter写一个文件选择按钮很简单,但字体、布局和高分屏支持都很难看;PyQt5功能强大,可它的GPL授权对需要分发源码的项目不友好。PySide6是Qt官方支持的Python绑定,LGPL协议、API和PyQt5基本一致,适合课程设计和公司内部工具。选型对比:

方案协议界面表现安装与打包
TkinterBSD简陋,控件少内置,无需额外打包
PyQt5GPL/商业成熟,控件多打包体积中等
PySide6LGPL与Qt一致,支持QML与PyQt基本一致

本项目GUI源码的主窗口代码如下,它负责界面布局:

import sys from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QVBoxLayout, QWidget from PySide6.QtCore import Qt class MaskDetectWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle('YoloV5口罩识别') self.setMinimumSize(800, 600) self.image_label = QLabel() self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.image_label.setText('请选择图片或视频') self.btn = QPushButton('选择图片') self.btn.clicked.connect(self.open_image) layout = QVBoxLayout() layout.addWidget(self.image_label) layout.addWidget(self.btn) widget = QWidget() widget.setLayout(layout) self.setCentralWidget(widget) def open_image(self): # 这里留空,后续代码会填充检测逻辑 pass if __name__ == '__main__': app = QApplication(sys.argv) window = MaskDetectWindow() window.show() sys.exit(app.exec())

这段代码把窗口、标签和按钮组合在一起,但还没有接入推理。合理的分层是:GUI只负责获取图片路径和显示结果,推理逻辑单独封装在Detector类里。这样如果要从图片改成摄像头输入,只需替换输入源,不需要改UI。

4.2 用QThread隔离推理,否则界面卡死

YoloV5推理如果直接在按钮的槽函数里执行,一张640x640图片在CPU上大约要跑0.2秒,GPU要0.02秒,看起来还能接受。但如果你在GUI里做视频检测,每帧推理加上绘制,主线程就会阻塞,窗口会出现未响应。常见做法是推理放到QThread里,用一个信号把结果传回主线程:

from PySide6.QtCore import QThread, Signal import numpy as np class DetectWorker(QThread): result_ready = Signal(object) # 发送检测结果 def __init__(self, detector, frame: np.ndarray, parent=None): super().__init__(parent) self.detector = detector self.frame = frame def run(self): # 在子线程中执行推理,避免阻塞UI results = self.detector.predict(self.frame) self.result_ready.emit(results)

在UI线程中,把worker实例化并连接result_ready信号,在回调函数里更新QLabel。注意不能在线程里直接调用label.setPixmap,因为Qt要求UI操作必须在主线程执行,否则会出现随机崩溃,错误信息可能是QThread: Destroyed while thread is still running。另一个坑是QThread对象不能作为局部变量,否则线程还没跑完就会被垃圾回收,解决方法是在窗口中用self.worker保存引用。

4.3 导出ONNX,用onnxruntime替代PyTorch做推理

训练出来的best.pt由PyTorch权重和网络结构两部分组成。如果把这个权重原样放进PyInstaller打包,整个程序体积会超过1.5GB,而且目标机器必须安装匹配的CUDA库。为了降低交付成本,可以把模型导出为ONNX:

# 导出为ONNX格式,便于在GUI中调用 python export.py --weights runs/train/exp/weights/best.pt --include onnx --img 640 --batch 1

导出后,在GUI中使用onnxruntime读取模型:

import onnxruntime as ort import cv2 import numpy as np class ONNXDetector: def __init__(self, onnx_path): providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] self.session = ort.InferenceSession(onnx_path, providers=providers) self.input_name = self.session.get_inputs()[0].name self.output_names = [o.name for o in self.session.get_outputs()] def predict(self, bgr_image): # 将BGR图像转为1/255归一化后的blob img = cv2.dnn.blobFromImage(bgr_image, 1/255.0, (640, 640), swapRB=True) outputs = self.session.run(self.output_names, {self.input_name: img.astype(np.float32)}) return outputs[0]

onnxruntime返回的outputs形状是[1, 25200, 7],其中25200是YoloV5在640分辨率下三个尺度输出的anchor总数,7是cx、cy、w、h、obj_conf、class0_conf、class1_conf。这个结果还不能直接画框,需要在predict方法后追加NMS。YoloV5官方仓库的non_max_suppression函数可以直接复制过来用,也可以把export.py参数加上--nms,让ONNX导出时内置NMS层。加入NMS后,输出会变成[1, 100, 6],处理起来更简单。这个思路在内存占用上明显优于直接加载torch模型,CPU单图推理可以控制在100毫秒左右。

5. 部署验证与常见坑:推理速度、误检和打包问题

5.1 端到端验证

模型训练和GUI封装完成后,最后必须跑一次真实视频流。先在验证集上跑detect.py,看看是否漏检:

# 在测试视频上验证模型效果 python detect.py --weights runs/train/exp/weights/best.pt --source test.mp4 --conf-thres 0.4 --iou-thres 0.5

conf-thres是置信度阈值,默认0.25。在答辩演示时为了减少误报,我会调到0.4。iou-thres用于NMS去重,0.5是常规值;如果画面中大量重叠的口罩遮挡,可以降到0.3,但可能把同一个口罩切成两个框。实际演示建议用摄像头实时检测:

# 打开0号摄像头实时检测 python detect.py --weights runs/train/exp/weights/best.pt --source 0

5.2 三个高频坑

表格里这三个坑,几乎每个跑YoloV5项目的人都会遇到:

现象原因解决方案
训练一半内存被打满--cache true把图片全缓存到内存改用--cache disk
摄像头画面黑屏或闪退0号摄像头被其他软件占用指定--source 1或先释放占用
打包后的GUI缺少onnxruntimePyInstaller没有收集动态库用pyinstaller --collect-all onnxruntime

第一个坑在笔记本上最常见。口罩数据集单张图片约300KB,几千张下来几个GB,但YoloV5会把预处理后的张量也缓存进去,实际内存占用是原始图片的3到5倍。第二个坑是Windows上摄像头占用,尤其微信或钉钉视频会议后,摄像头设备没有被释放,程序打不开video0。第三个坑在PyInstaller打包时很隐蔽,错误信息往往只提示找不到onnxruntime_providers_shared.dll,实际上是没收集onnxruntime的完整目录。

5.3 进阶:用一个权重跑多个场景

口罩识别只是一个基线。把mask.yaml换成基于yolov5的水果识别数据集、车牌识别数据集,训练流程不需要改,GUI后端只需要修改类别列表。如果想在Jetson Nano这类边缘设备上跑,推荐把导出参数改成--include engine,生成TensorRT加速权重,然后使用NVIDIA的TensorRT推理后端。在Jetson上执行python export.py之前,先运行sudo jetson_clocks避免降频,否则导出engine的时间会翻倍。这个迁移思路,对任何基于YoloV5的目标检测项目都适用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询