简介:基于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 error | torchvision与torch版本不匹配 | 对照官方requirements重新安装 |
| Label class x exceeds nc | txt标签中的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 valval.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基本一致,适合课程设计和公司内部工具。选型对比:
| 方案 | 协议 | 界面表现 | 安装与打包 |
|---|---|---|---|
| Tkinter | BSD | 简陋,控件少 | 内置,无需额外打包 |
| PyQt5 | GPL/商业 | 成熟,控件多 | 打包体积中等 |
| PySide6 | LGPL | 与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.5conf-thres是置信度阈值,默认0.25。在答辩演示时为了减少误报,我会调到0.4。iou-thres用于NMS去重,0.5是常规值;如果画面中大量重叠的口罩遮挡,可以降到0.3,但可能把同一个口罩切成两个框。实际演示建议用摄像头实时检测:
# 打开0号摄像头实时检测 python detect.py --weights runs/train/exp/weights/best.pt --source 05.2 三个高频坑
表格里这三个坑,几乎每个跑YoloV5项目的人都会遇到:
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 训练一半内存被打满 | --cache true把图片全缓存到内存 | 改用--cache disk |
| 摄像头画面黑屏或闪退 | 0号摄像头被其他软件占用 | 指定--source 1或先释放占用 |
| 打包后的GUI缺少onnxruntime | PyInstaller没有收集动态库 | 用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的目标检测项目都适用。
本文还有配套的精品资源,点击获取