
简介一套基于深度学习的口罩检测系统完整项目包面向计算机视觉方向课程设计、期末大作业及毕业设计利用YOLOv3等目标检测算法解决公共场景下人员是否佩戴口罩的实时识别问题。资源共45个文件压缩包约2.64MB包含Python源码、模型配置、数据列表及说明文档其中py脚本覆盖训练、检测、数据预处理和格式转换等环节cfg文件对应YOLOv3、Darknet53及YOLOv3-Tiny三种网络结构txt文本数据用于划分训练集、验证集和测试集jpg/png图片可直接用于效果测试。项目提供了从模型训练、锚点聚类、批量检测到视频推理的完整流程同时包含说明文档和运行配置指引可直接对照运行适合希望深入理解YOLO原理与工程实现的读者。目前已有41人学习下载可作为课程设计或毕业设计的参考基础。1. 拿到“基于深度学习的口罩检测系统.zip”之后别急着解压跑起来很多人拿到这个压缩包的第一反应是解压然后找到train.py就直接敲下去。真正折腾过的人都知道这类包解压后往往缺依赖、路径不对、标注格式不统一模型动不动 mAP 为 0。所谓“基于深度学习的口罩检测系统”本质上就是一套把数据标注、卷积神经网络训练、推理可视化串起来的完整项目。它能解决的是在图片、视频甚至摄像头画面中实时框出人脸并区分“戴了口罩”和“没戴口罩”。对准备毕设或第一次接触深度学习项目的人来说最有价值的不是某一段代码而是整套流程如何跑通。这篇按照接手项目时的常见顺序来讲从环境配置到训练调参再到避坑照着做就能交付一个能演示的检测系统。2. 先把 zip 里的项目结构读明白模型选型与代码主线拿到基于深度学习的口罩检测系统.zip后不要急着点解压最好先用命令行把压缩包放到英文路径下。Windows 或 Linux 里中文目录名看上去没事但 OpenCV 和 PyTorch 在读文件时经常因为编码问题读不到图片最后报一堆奇怪的unable to decode image错误。下面这段命令能少走很多弯路。# 复制到干净的英文路径避免中文目录名导致 cv2.imread 读不出图 cp 基于深度学习的口罩检测系统.zip /home/user/projects/mask_det_kit cd /home/user/projects/mask_det_kit unzip 基于深度学习的口罩检测系统.zip -d mask_src cd mask_src # 只看两层目录把缓存和训练输出过滤掉剩余结构一眼就能看清 tree -L 2 --dirsfirst -I __pycache__|*.pyc|runs这段命令把压缩包解压到mask_src目录然后过滤掉__pycache__、*.pyc和runs。runs是训练过程中产生的事件文件和权重目录体积很大先不看反而清爽。解压之后如果发现train.py在根目录说明项目组织得算规整如果散落在code/、src/里也不奇怪很多课程设计包并没有统一规范。2.1 先找三个核心模块训练入口、推理入口和数据集一个能跑的深度学习口罩检测系统压缩包里至少要有三类东西。第一类是数据通常放在data/images和data/annotations下标注格式可能是 VOC 的 XML也可能是 YOLO 的 TXT。第二类是训练入口常见文件名为train.py负责读入图片和标签更新模型权重。第三类是推理入口常见文件名为detect.py、inference.py或webcam.py负责加载训练好的权重对输入图片或摄像头画面画框。下面这张表是我每次打开压缩包都会对照检查的清单文件/目录作用如果缺失怎么办train.py或training/模型训练入口找main.py、run.pydetect.py或inference/推理可视化入口找video_demo.pydata/images原始图片集找JPEGImages或datasetdata/annotations标注文件找labels或Annotationsrequirements.txtPython 依赖清单没有就自己按 import 补weights/预训练或训练好的权重没有就下载 YOLO 官方预训练权重这一步的核心目的是确认这个 zip 是完整可复现包还是一个只有 UI 的空壳。很多课程设计包会故意不附全量数据只给几张示例图片理由是文件太大。但训练脚本、推理脚本和标注格式样例必须齐全否则我们只能把缺的部分自己重写工作量几乎等于做半个项目。确认文件齐全后再打开requirements.txt看版本。常见情况是torch1.10.0、torchvision0.11.0、numpy1.21.6。新环境默认装 PyTorch 2.0虽然大部分接口兼容但torchvision.transforms的行为有变化比如某些函数被删除代码一跑就崩。先看版本再决定是装旧版本还是改代码这是“深度学习项目”落地时最先要做的事。2.2 为什么口罩检测项目普遍选 YOLO而不是 Faster R-CNN在深度学习目标检测领域口罩检测是典型的实时目标检测任务。主流有两类算法两阶段检测器以 Faster R-CNN 为代表一阶段检测器以 YOLO、SSD 为代表。两阶段检测器先提取候选区域再对每个候选区域做分类和回归精度高但计算量大在普通笔记本上跑摄像头视频很难达到实时。YOLO 把目标检测看作单次回归问题一张图一次前向同时输出类别和边界框速度可以做到几十 FPS精度足够应付口罩识别。从模型结构看YOLO 系列通常由骨干网络、颈部网络和检测头组成。骨干网络负责提取特征比如 CSPDarknet颈部网络负责多尺度特征融合典型结构是 FPN检测头输出 anchor 框的类别概率和坐标偏移。训练时用 BCE 损失做分类用 CIoU 损失回归边框。这些概念在“深度学习课本”和“动手深度学习”教程里都有对应章节但落到代码上YOLO 的工程封装程度比 Faster R-CNN 高得多这也是毕设包大量采用它的直接原因。对比下来实际选型可以参考这张表对比维度YOLOv5sFaster R-CNNSSD模型体积约 14 MB约 108 MB约 90 MB推理速度30 FPS 以上5~10 FPS25 FPS 左右训练难度中低高中社区资料多中少如果一个压缩包里同时有 YOLOv5 和 EfficientDet我会优先用 YOLOv5。因为它的train.py封装得很完整数据增强、早停、mAP 计算都是开箱即用。你不需要把整个网络重写只要把自己的标注数据塞进datasets目录改一个 YAML 文件就能训练。当然 YOLO 也不是万能的。对特别小的目标比如远处人脸戴着黑色口罩模型容易漏检。此时可以考虑把输入分辨率提升到 1280或者换用yolov5m以上的模型。可如果数据集只有几百张图片换模型并不会带来本质提升反而更容易过拟合。这一章讲清楚了模型选型下一步就是把运行环境搭起来让代码真正跑起来。3. 环境配置与数据集准备复现深度学习项目的第一道坎口罩检测项目最耗时间的部分通常不是模型设计而是“深度学习环境配置”和数据集整理。很多同学把 zip 解压后直接全局pip install结果把系统环境搞乱最后只能重装 Python。我的习惯是所有深度学习项目都用一个独立 conda 环境即使同时做几个项目也不会互相干扰。3.1 用 conda 创建 Python 3.8 环境并按项目版本装 PyTorch下面这段命令适用于绝大多数基于 PyTorch 的口罩检测项目。选择 Python 3.8是因为 PyTorch 1.8 到 2.0 之间的版本对 Python 3.8 支持最完整后续换包也灵活。# 创建独立环境避免污染系统自带 Python conda create -n mask_det python3.8 -y conda activate mask_det # 安装 PyTorch这里使用 CUDA 11.3 对应的 wheel # 如果只有 CPU去掉 cu113 装 CPU 版 pip install torch1.10.0 torchvision0.11.0 -f https://download.pytorch.org/whl/torch_stable.html # 读取项目自带依赖列表安装 opencv、numpy 等 pip install -r requirements.txt为什么用-f指定 wheel 地址因为 PyTorch 官方把不同 CUDA 版本的安装包放在同一个索引页面里默认 PyPI 上只有 CPU 版或特定版本。-f指定后才能装到带 CUDA 的版本。这里有个细节电脑装了最新的 CUDA 12不代表不能跑 PyTorch 1.10PyTorch 运行时会根据显卡驱动向下兼容只要驱动版本大于等于 11.3就可以正常调用 GPU。安装完成后立刻验证环境是否真的可用python -c import torch; print(torch.__version__, torch.cuda.is_available()) # 输出 True 说明能调用 GPU输出 False 说明只能跑 CPUtorch.cuda.is_available()是排查环境问题的关键命令。输出 False 时原因通常是安装的是 CPU 版而不是 GPU 版。此时不要急着重装用pip list | grep torch查看包名再做决定。还要注意 numpy 版本。opencv-python 和 PyTorch 对 numpy 要求不同现在 pip 默认解析可能把 numpy 升到 2.x导致cv2在 import 阶段报np.float不存在之类的问题。解决方法是锁定版本pip install numpy1.23.5我一般会在requirements.txt最后补一行numpy1.23.5这样后续不管安装什么依赖都不会把它偷偷升级。这个配置问题在“深度学习环境配置”阶段至少能拦下一半人。3.2 把公开口罩数据集转成 YOLO 格式目录与标注文件一并对齐环境只是暖场真正的难点在数据集。一份可用的口罩检测数据集图片和标注必须严格对应。如果压缩包里的标注是 VOC XML需要转换成 YOLO TXT。YOLO TXT 每行表示一个目标格式为class_id x_center y_center width height其中类 ID 从 0 开始坐标除以图片宽高后归一化。下面是一个转换脚本保存为voc2yolo.py就能直接用# 将 VOC XML 标注转成 YOLO txt 格式 # 用法: python voc2yolo.py --xml_dir annotations --out_dir labels import os import argparse import xml.etree.ElementTree as ET from glob import glob classes [mask, no_mask] def convert(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() img_w float(root.find(size/width).text) img_h float(root.find(size/height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in classes: continue cls_id classes.index(name) box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) # 把左上角/右下角坐标转成中心点宽高并归一化 x_center (x1 x2) / 2.0 / img_w y_center (y1 y2) / 2.0 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) if not lines: return txt_name os.path.basename(xml_path).replace(.xml, .txt) with open(os.path.join(out_dir, txt_name), w) as f: f.write(\n.join(lines)) if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(--xml_dir, requiredTrue) parser.add_argument(--out_dir, requiredTrue) args parser.parse_args() os.makedirs(args.out_dir, exist_okTrue) for xml_path in glob(os.path.join(args.xml_dir, *.xml)): convert(xml_path, args.out_dir)脚本的核心逻辑是遍历所有 XML解析每个目标的name和bndbox然后把矩形框转成中心点宽高的归一化表示。这里最重要的参数是classes列表它的顺序决定了标签文件里的数字含义。如果 XML 里类别名是with_mask、without_mask而classes里写的是mask、no_mask那么name not in classes会直接跳过这些标注导致大量图片没有任何标签mAP 自然为 0。转完格式后最好写一个小工具把 YOLO txt 重新画回图片上检查标注位置是否准确这比直接开训省时间得多# 把 YOLO 标签画到原图上检查转格式后是否有偏移 import cv2 img cv2.imread(datasets/images/train/00001.jpg) h, w img.shape[:2] with open(datasets/labels/train/00001.txt) as f: lines f.read().strip().splitlines() for line in lines: cls_id, cx, cy, bw, bh map(float, line.split()) x1 int((cx - bw / 2) * w) y1 int((cy - bh / 2) * h) x2 int((cx bw / 2) * w) y2 int((cy bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imshow(check, img) cv2.waitKey(0)这段代码读一张标注文件把归一化坐标还原成像素坐标画出绿色矩形框。如果框和人脸明显错位说明转换脚本里宽高计算出了问题通常是忘记除以图像尺寸或者在复制图像时对图片做过裁剪。把标注可视化这一步称作“深度学习实战项目案例”里的标准动作不过分它能让原始数据质量在开始训练前就暴露出来。转换完成后按 8:1:1 切分训练集、验证集、测试集。切分时如果直接random.shuffle会导致每次执行结果不同不方便复现。正确做法是设置固定随机种子或者在第一次切分后把文件名单保存下来。YOLO 目录结构如下datasets/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── mask.yamlmask.yaml是训练时的数据配置内容很简单# 口罩检测数据配置目录路径要写成绝对路径或当前工作目录下的相对路径 train: datasets/images/train val: datasets/images/val nc: 2 names: [mask, no_mask]到这里数据准备才算真正结束。现在可以进入训练环节。4. 训练与评估让模型真的能分清“戴”和“没戴”数据准备就绪后训练本身只是一条命令的问题。这一章讲清楚训练命令背后的参数以及训练日志里哪些数字值得看。要知道跑通一次训练只算起步能把模型调到可展示的程度才是关键。4.1 用 YOLOv5 训练口罩模型一条命令和它背后的参数如果压缩包里不带训练代码可以考虑补一套 YOLOv5 开源代码这也是社区里常见做法。训练命令通常长这样python train.py --data mask.yaml --weights yolov5s.pt --img 640 \ --batch-size 16 --epochs 100 --workers 4 \ --device 0 --project runs/train --name mask_01--data指向数据配置文件它告诉训练器去哪里找图片和标签。--weights是指定的初始权重yolov5s.pt是 COCO 预训练模型比从零开始收敛快很多稳定性也高。--img是输入分辨率640 是速度和精度的常见折衷显存小于 6GB 时改到 512 会更稳。--batch-size决定一次送入多少张图片显存不够就调小网络梯度抖动增大时再配合学习率调整。--workers是并行加载数据的进程数Windows 上如果报 DataLoader worker 错误改成 0 或 2。--device 0指定第一张显卡没有 GPU 时改成--device cpu但训练速度会慢很多。直接启动训练后终端会刷日志。如果出现All 2 classes说明标签读取成功如果出现WARNING: 0 labels found说明标签路径或标注内容有问题此时要立刻停下检查。这个警告比训练到一半发现 mAP 为 0 省心得多一定要重视。4.2 训练过程中真正要盯的四个指标和 early stop训练日志每个 epoch 都会刷出一串指标但真正决定系统能不能用的只有四个Precision、Recall、mAP0.5和mAP0.5:0.95。指标含义数值异常时怎么处理Precision检测出的框里有多少是真目标偏低时提高置信度阈值或增加负样本Recall真目标里有多少被检测出来偏低时降低置信度阈值或增加正样本mAP0.5IoU 大于 0.5 就算对时的平均精度总体验收指标0.85 以上即可用mAP0.5:0.95IoU 从 0.5 到 0.95 的平均值框位精度实时系统不必强求口罩检测这种场景mAP0.5 达到 0.85 以上基本就能演示了不用刻意追求 mAP0.5:0.95。后者对框位置要求非常苛刻实时视频里小目标本来就容易抖动继续压这个指标收益不大。训练过程中前 20 轮 mAP 通常会快速上升30 轮后开始平缓。YOLOv5 默认patience100意思是 100 轮内 mAP 不再上升就早停。自己写训练脚本时不要删掉早停至少留下一个patience30省电也省时间。训练结束后runs/train/mask_01/weights下会有best.pt和last.pt只用best.pt做推理。如果数据量只有几百张训练很容易过拟合。这时候不要急着换更深的骨干网络先做数据增强。YOLOv5 自带 mosaic、翻转、HSV 扰动可以通过修改超参数文件打开或增强。常见做法是复制一部分“没戴口罩”的样本做数据重采样然后继续训练。所谓“深度学习对比试验”也最好固定一组数据只改一个模型参数否则你根本分不清精度变化来自数据还是模型。4.3 类别不平衡和难样本容易被忽略的最后一公里口罩检测数据集里“戴口罩”样本通常远多于“没戴口罩”样本。模型训练出来之后可能把所有脸都判成“mask”不戴口罩的人也变成绿色框这就失去了告警意义。解决不平衡常见有三种方式。第一种是直接做重采样把no_mask图片复制几份放入训练集简单有效但要注意复制的图片不要和验证集重合。第二种是在数据增强时提高难样本的出现概率比如对no_mask图片做更大角度的随机裁剪。第三种是调整损失函数中的类别权重给少数类更高的cls_loss系数。对课程设计来说第一种方式最稳妥也最容易被评委听懂。完成训练后可以拿一张未戴口罩的照片做可视化。如果框住并且标成no-mask说明模型已经学到有效特征如果漏掉回到数据增强和分辨率这两方面去调。这样你交付的不再只是一个 zip而是一个能现场演示的“基于深度学习的口罩检测系统”。5. 口罩检测系统避坑指南五个让项目翻车的真实案例这一章是我做项目时反复翻看的笔记。下面五个问题在“基于深度学习的口罩检测系统.zip”里出现频率极高每一条按现象、原因、解决来写。5.1 训练十几个小时 mAP 还是 0类别编号对不上现象训练脚本跑得动loss 也在降但每个 epoch 结束 mAP0.5 一直显示 0预测结果全空白。原因数据集标签文件里的数字类别编号和训练配置里的names对不上。比如标注里without_mask被转成 class_id0但训练 YAML 里names: [mask, no_mask]的第 0 个位置是mask。模型把戴口罩的框当成不戴把不戴的框当成戴最后网络不知道该学什么。解决写一个简单命令统计每个标签文件的类别编号分布cat datasets/labels/train/*.txt | awk {print $1} | sort | uniq -c如果输出不是0和1两类或者数量不合理就要回到转换脚本里的classes列表调整顺序。遇到类别名不一致提前做好映射比如把with_mask、face_with_mask统一映射为 0把without_mask、face_no_mask映射为 1。我最早做 VOC 转 YOLO 时就在这个坑里待过两天损失函数一切正常模型却什么都没学到。5.2 推理时检测框乱跳预处理没有做 letterbox现象单张图片测试正常但把模型接到视频或摄像头上后目标稍微移动一下框就乱抖或者坐标偏移到画面右下角。原因模型训练时输入是 640x640 的等比例缩放填充图推理时却用cv2.resize(img, (640, 640))把原图压扁了。检测出的坐标是在 640x640 图上的坐标没有做逆映射于是框的位置严重偏移。视频场景中人物比例不断变化偏移看起来就像乱跳。解决推理前使用 letterbox按原图长宽比缩放到 640 边界剩余部分用灰色填充推理后按比例把坐标映射回原图。典型实现如下# 保持长宽比缩放不足部分填充灰色 import cv2 def letterbox(img, new_size640): h, w img.shape[:2] scale new_size / max(h, w) new_w, new_h int(w * scale), int(h * scale) resized cv2.resize(img, (new_w, new_h)) canvas cv2.copyMakeBorder(resized, top(new_size - new_h) // 2, bottom(new_size - new_h 1) // 2, left(new_size - new_w) // 2, right(new_size - new_w 1) // 2, borderTypecv2.BORDER_CONSTANT, value(114, 114, 114)) return canvas, scale # 拿回检测框后需要根据填充位置换算原始坐标 # 原图坐标 (yolo_x - pad_left) / scale这里要注意填充像素的奇偶问题。bottom和right的1是为了应对奇数分辨率如果忽略坐标会偏 1 像素短期看没事对多帧视频累积起来就是抖动。很多开源代码训练时用了 letterbox推理端只写一句resize结果模型精度很高但框位置不准。这个问题在摄像头演示时最容易暴露提前处理掉能省很多麻烦。5.3 换数据集后模型不收敛学习率继承了两套超参现象用别人训练好的权重继续训自己的数据前 10 轮 loss 完全没有下降甚至越来越大最后训练崩掉。原因两个可能。一是学习率设置不对YOLOv5 默认学习率是按batch-size16推算出来的如果把 batch-size 改成 4 却没有降学习率梯度噪声变大模型不稳定。二是在加载别人权重时类别数不一致虽然代码会自动裁剪最后一层但新层初始化是随机的学习率太大会破坏骨干特征。解决先确认自己的数据类别数再使用通用预训练权重而不是某个口罩专用权重。小数据集建议把--lr0调到 0.005并开启 warmup让前几个 epoch 学习率从小逐渐升到目标值。YOLOv5 默认有 warmup但手写训练脚本时很容易漏掉可以手动加# 前 3 个 epoch 线性升高学习率防止起步就把权重冲乱 if epoch 3: lr base_lr * (epoch 1) / 3 for g in optimizer.param_groups: g[lr] lr更稳的做法是先跑 10 轮小批量实验看 loss 是否下降再决定要不要继续。不要一上来就复制别人的超参配置先跑通最小实验再扩展。5.4 视频检测卡到没法看预处理和模型并行没做好现象训练好的模型跑单张图片速度不错但接入摄像头后画面一卡一卡延迟超过两秒。原因摄像头画面分辨率高直接每帧推理开销很大。同时循环里对每一帧做cv2.cvtColor、转张量、.item()等串行操作再加上模型没有放到 GPU 上自然跑不动。解决第一输入帧先用 letterbox 缩放到 416x416这个分辨率对口罩检测足够第二模型支持半精度就用model.half()提升推理速度第三视频流采用跳帧策略每处理一帧跳过两帧中间显示上一帧结果即可。代码层面要注意模型加载必须在while循环之前且推理要加上torch.no_grad()with torch.no_grad(): preds model(img)只加这一行推理帧率就可能提升 20% 以上因为梯度计算被彻底关掉显存占用和耗时都会下降。再配合跳帧摄像头画面基本可用。这类优化细节在“深度学习项目”里经常被忽略但它决定演示现场是否翻车。5.5 GUI 一点“开始检测”就卡死UI 线程和推理线程没分离现象用 Tkinter 或 PyQt 写了界面点按钮后窗口立刻无响应有时还会闪退。原因把while True摄像头循环直接挂在按钮回调里UI 主线程被推理循环阻塞。GUI 框架需要不断处理窗口消息一旦主线程被占用窗口就会显示“未响应”并可能被杀掉。解决把视频读取和模型推理放到子线程通过队列把结果帧传回 UI 线程。子线程负责摄像头读帧、推理和画框UI 线程只负责取队列的最新画面并刷新。# 共享队列会在测试时出现放入 result_frame import threading import queue import cv2 def worker(shared_queue): cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break # 这里调用模型推理并以 result_frame 替代画框结果 # result_frame detect_and_draw(frame) shared_queue.put(result_frame) # 在 GUI class 中周期刷新只保留最新一帧 def update_frame(self): while not self.q.empty(): self.latest_frame self.q.get_nowait() if self.latest_frame is not None: self.show_image(self.latest_frame) self.root.after(10, self.update_frame)队列缓存不能无限增长否则延迟越来越大。所以每次刷新时把队列里旧帧全部取走只显示最新的一帧。这个细节如果不处理运行几分钟后画面会越来越迟最终看起来像卡死。我在“深度学习毕设”答辩前踩过一次现场演示时界面崩掉非常狼狈。6. 把模型变成能给别人演示的服务导出和告警逻辑训练完best.pt距离“能给别人演示”还差两步模型导出和业务逻辑。我习惯先把 PyTorch 权重导出成 ONNX这样部署到另一台电脑时不必再装完整 PyTorch 环境。python export.py --weights runs/train/mask_01/weights/best.pt --img 640 --include onnx导出完成后用 ONNX Runtime 跑一张图片确认输出和 PyTorch 推理结果接近。如果精度差异明显优先检查预处理归一化参数是否一致特别是(x / 255.0)和标准差有没有被忽略。更进一步可以在画面上叠加统计信息比如显示区域人数、未戴口罩人数并把没戴口罩的人用红色框标出。实现时把检测逻辑拆成三步预处理、推理、后处理。这样以后把 YOLOv5 换成 YOLOv8只需要改中间的推理部分前后处理可以复用。我自己的教训是第一版演示效果很差原因是没有对检测结果做置信度过滤。把模型输出 0.25 置信度的框全画出来结果背景里类似的物体也变成了目标。后来把conf_thres0.45、iou_thres0.5写死画面立刻干净很多。调阈值没有固定公式可以先在验证集上跑一遍找到 Precision 和 Recall 的平衡点。这套路径走完之后你手里那份 zip 就不只是一个压缩包了。从数据转格式到训练调参再到摄像头实时检测整条链路是闭环的。拿这些内容去答辩重点讲清楚你踩过的坑和每一步选择依据比堆一堆训练日志更有说服力。希望这些经验和代码能帮你少走一段弯路也希望你按这个方向深入下去时能做出自己的改进。本文还有配套的精品资源点击获取