简介:面向高校毕业设计、课程设计与期末大作业的深度学习实战项目,基于YOLO目标检测算法实现口罩佩戴识别,适用于校园、工地、商场等场景的监控与考勤辅助,也适合已有Python基础、希望快速上手计算机视觉的初学者与开发者。项目内置YOLOv3、Darknet53、YOLOv3-Tiny三套模型配置,配套训练、预测、批量检测、格式转换等Python脚本,可完整跑通从数据标注到模型推理的流程。压缩包共45个文件,包括15个Python脚本、12个文本标签/数据文件、6个pyc依赖、3个cfg模型参数及测试图片等,整体仅2.64MB,结构清晰、类型分明,便于按需学习。当前已有41人学习使用。资料还提供kmeans锚点聚类、VOC数据集处理、darknet权重转换等工具,并附README说明文档,可帮助理解锚点生成、数据增强、模型微调等关键细节,是完成课程项目或入门目标检测的实用参考。
1. 口罩检测系统,究竟在解决谁的什么问题
从校门口刷脸测温的闸机,到建筑工地进场打卡的摄像头,再到写字楼前台的访客通道,“有没有戴口罩”已经是最常见的视觉判断场景之一。一个基于深度学习的口罩检测系统,本质上就是拿一批标注好“戴/未戴/戴错”的图片,训练出一个小模型,然后部署到普通摄像头或者边缘盒子上,做到实时判断、触发语音提示或门禁联动。你在网上看到的“基于深度学习的口罩检测系统.zip”并不是某个天才发明的黑科技,它通常是一整套可跑的方案:数据集整理脚本、模型训练代码、推理脚本和部署说明打包在一起,适合毕业设计二次开发,也适合拿来当工业智能安防的基线,替换掉过去靠红外测温仪顺带观察的土办法。这篇笔记我就顺着“拿到压缩包之后怎么用”的思路,把选型、训练、部署和踩坑一起讲透。
2. 选型与数据:为什么是YOLOv8,以及数据怎么准备
2.1 先定模型边界:口罩检测为什么被“小目标”和“多尺度”卡住
口罩检测不是简单的二分类问题。一张画面里可能有远景的几个人头,近处又有人侧着身,口罩在脸部的占位可能只有几十个像素,传统手工特征根本扛不住。深度学习目标检测模型里,Faster R-CNN这类两阶段方法精度好,但推理速度在边缘设备上很难做到每秒25帧以上;SSD轻快但小目标召回率差。口罩检测恰恰要同时处理远近不同的人脸,所以目前从业者手里的方案,绝大多数已经收敛到了YOLO系列,尤其是YOLOv8或YOLO5。
YOLOv8把分类头和回归头解耦,anchor-free机制让它在小目标上比旧版YOLOv5更稳,模型导出时可以直接出onnx、tensorrt,后续部署省掉大量格式踩坑。我一般会先做一个快速对比实验:用同一份5000张数据集,分别跑YOLOv8n和Faster R-CNN,前者在RTX3060上训练一版的用时大概是后者五分之一,推理速度接近十倍差距,mAP@0.5在口罩这类相对规整的目标上反而能高出1到2个点。这就是选择YOLOv8的核心理由:不是因为它最准,是因为它在“精度、速度、部署成本”这三个条件上最平衡,适合作为一套系统的默认底座。
2.2 数据集与标注格式:从VOC转YOLO是每个人都会遇到的坎
如果你下载的压缩包自带了一份数据集,跳过这一步;但多数公开口罩数据集用的是VOC格式,也就是xml标注文件,而YOLOv8训练又默认要求txt格式的YOLO格式标注。即使压缩包没有这份数据,也建议自己标注一小批补充数据,因为公开集里“口罩戴错”这类样本往往特别少。我常用的转换逻辑是读取xml里的object坐标,统一归一化到图片宽高,然后写入txt文件,每一行是“class_id x_center y_center width height”。
下面这段转换脚本是整套系统里最常被反复改的工具,代码直接用Python标准库就能跑,不依赖额外包,适合先放在数据处理目录里让数据集制作“后悔药”随时可用。
import os import glob import xml.etree.ElementTree as ET # 需要修改的三个路径:xml目录,保存txt的目录,类别名列表 xml_dir = "./annotations" # 输入的VOC xml目录 txt_dir = "./labels" # 输出YOLO txt目录 class_names = ["with_mask", "without_mask", "mask_weared_incorrect"] # 类别顺序必须后续与训练yaml一致 os.makedirs(txt_dir, exist_ok=True) def convert_xml_to_yolo(xml_path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in class_names: continue class_id = class_names.index(name) box = obj.find("bndbox") xmin = float(box.find("xmin").text) / img_w ymin = float(box.find("ymin").text) / img_h xmax = float(box.find("xmax").text) / img_w ymax = float(box.find("ymax").text) / img_h w = xmax - xmin h = ymax - ymin # 修正truncated边缘,避免训练时出现负数或不合法宽度 x_center = xmin + w / 2.0 y_center = ymin + h / 2.0 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) w = min(max(w, 0.0), 1.0) h = min(max(h, 0.0), 1.0) lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") return lines for xml_file in glob.glob(os.path.join(xml_dir, "*.xml")): txt_name = os.path.basename(xml_file).replace(".xml", ".txt") lines = convert_xml_to_yolo(xml_file) with open(os.path.join(txt_dir, txt_name), "w") as f: f.write("\n".join(lines))转换脚本有两个必须盯紧的参数。第一,类别列表的顺序要在后续训练用的自定义data.yaml里保持一致,否则推理时框和标签完全对不上,这是最常见的“挂羊头卖狗肉”报错。第二,我在脚本里加了坐标夹紧操作,因为公开数据集里偶尔有标注出界的点,如果不修正,训练时损失函数会跳到nan。你也许会顺手用labelimg重新标注一批自己的现场图,记得导出格式选择YOLO,这样免去转换步骤,还能避免VOC里name大小写不一致造成的麻烦。
2.3 数据增强的尺度:口罩目标太小,马赛克增强反而容易误杀
训练脚本默认会开YOLOv8自带的马赛克增强,也就是把四张图拼成一张去训练。这招对常规目标很有效,但对于“远景小脸 + 口罩”的场景容易出问题:四张图拼完之后,原图里本来只有32像素的口罩区域被缩小到16像素,模型学到的是模糊纹理,而不是口罩特征。我的习惯是在处理口罩数据集时,先把马赛克增强概率从默认1.0降到0.2,同时加大mosaic之后的一次随机裁剪比例。另一个值得开的增强项是HSV微调,因为不同工地的光线环境差异大,颜色饱和度和亮度变化能帮助模型适应夜间弱光。
补充一份自己的样本来做困难样本挖掘尤其重要。压缩包里如果带了训练脚本,别马上全量训练,先挑出那些“远处三个人排成一队”的图片,单独做一个验证切片。很多基于深度学习的口罩检测系统在公开测试集上mAP很高,但现场一开摄像头就发现两个人前后重叠时漏检严重,原因就是训练数据里近景大头占多数,模型没有见过中远景多人交互的pattern。把这个维度的数据补齐,比盲目换网络结构管用得多。
3. 训练这套模型的本地与云服务器参数:最小可跑命令与三个必调项
3.1 从压缩包到第一条训练命令
拿到压缩包以后,本地环境首先要把依赖装好。常见做法是使用miniconda建一个干净环境,然后安装pytorch、ultralytics、opencv-python、onnxruntime这几个核心包。很多翻车现场都出在pytorch版本和显卡驱动不匹配上,所以我的流程是先跑一段nvidia-smi,确认自己的CUDA版本,再选择对应的pytorch安装命令。如果你手里只有一台CPU笔记本,照样能训练,但通常建议用云平台租GPU,深度学习环境配置这件事,GPU场景和CPU场景完全是两种节奏。
训练脚本本身并不复杂,压缩包里一般会有类似train.py或直接调用ultralytics YOLO接口的入口。下面是我基于ultralytics封装过的一套最小训练命令,它把数据集路径、轮数、分辨率、batch size都暴露成命令行参数,方便你在调试时不用反复改主脚本。
# train.py from ultralytics import YOLO import argparse parser = argparse.ArgumentParser() parser.add_argument("--data", type=str, default="datasets/data.yaml") parser.add_argument("--weights", type=str, default="yolov8n.pt") parser.add_argument("--imgsz", type=int, default=640) parser.add_argument("--batch", type=int, default=16) parser.add_argument("--epochs", type=int, default=100) parser.add_argument("--device", type=str, default="0") args = parser.parse_args() model = YOLO(args.weights) # 加载预训练权重,不是从零训练 model.train( data=args.data, imgsz=args.imgsz, batch=args.batch, epochs=args.epochs, device=args.device, workers=4, patience=15, project="./runs/train", name="mask_exp", )注意第三个参数、第五个参数:imgsz和batch是口罩检测训练里影响结果最大的两个旋钮。imgsz默认640,但如果你的摄像头画面里人脸经常很小,推荐升到768或832,代价是训练时间增加约20%,推理速度下降;反过来如果只识别近距离闸机,640完全够用。batch一定不要为了塞满显存无脑开大,口罩数据集里背景相似度高,过大batch容易让模型收敛进一个平坦的局部最小值,导致验证集mAP反而更低。我一般从16起步,看到loss曲线震荡后在8、24之间试探。
weights参数也值得说一句。强烈建议使用yolov8n.pt或yolov8s.pt作为预训练权重,而不是随机初始化。COCO预训练模型已经学会了人脸轮廓、布料纹理这类底层特征,口罩检测属于典型的下游迁移任务,不用白不用。如果你是做毕设或竞赛,追求报告里一个好看的数字,那就用s甚至m模型;如果目标是部署到Jetson或树莓派,尽量锁在n规模,不然后续TensorRT推理帧率会很难看。
3.2 数据yaml与三类样本的类别平衡
训练前你还需要一份和数据集匹配的data.yaml,这个文件决定了模型知道该预测几个类别、训练集和验证集分别在哪里。口罩检测数据集最常见的标签是三类:with_mask、without_mask、mask_weared_incorrect。我建议保留三类而不是简化为两类,因为“戴错口罩”的工程价值很高,例如鼻子露在外面这类情况,只有三类模型才能提示用户纠正佩戴姿势。下面是实际在用的yaml模板,和前面转换脚本里的类别顺序保持一致。
# data.yaml path: ./datasets # 数据集根目录 train: images/train # 训练图片目录 val: images/val # 验证图片目录 test: images/test # 可选测试集 nc: 3 # 类别数量 names: 0: with_mask 1: without_mask 2: mask_weared_incorrect路径这块有个高频坑:path字段一旦写错,后续都说找不到图片。最好用相对当前工作目录的路径,避免硬编码别人的绝对路径。另外检查一下验证集是否有错标样本,尤其“戴错口罩”这个类别最容易标错,标注者常常把口罩拉到下巴的图标成without_mask,这会直接污染模型对两类边界的判断。我自己跑过一轮训练,发现val mAP卡在0.82上不去,后来逐张检查验证集图片,发现约3%的without_mask标签里混着口罩下滑到颈部的图,手工修正后mAP立马上到0.87。
3.3 断点恢复与早停:省时间的关键参数
训练口罩检测这类中小规模数据集,不需要每次都把100个epoch跑完。patience=15意思是连续15个epoch验证集mAP没有提升,训练就自动停止。这个参数是压缩包里的“后悔药”,能让你避免周末挂着训练、周一发现早就在过拟合。断点恢复可以这样用:如果训练到一半意外中断,不需要重头开始,运行同一条训练命令时会自动从runs/train/mask_exp/weights/last.pt接着训练。另外你要养成每轮看训练完的results.png而不是只看控制台loss的习惯,图中左上角的train/box_loss如果持续下降但val/box_loss在第40个epoch开始反弹,说明模型开始学习背景噪声,这时应该停掉、调低学习率或加大数据增强。
还有一个参数是cos_lr=True:我常用余弦学习率衰减替代默认线性衰减,它在训练后期能多挤0.5到1个点mAP。如果你手头有多张显卡,device参数可以写0,1,但口罩检测数据集通常不大,单卡足够,多卡的数据同步反而会引入额外的调参时间。不要把毕设作品幻想成一定要分布式训练,很多系统压缩包里声称的“分布式”,实际落地还是单卡更稳。
4. 部署落地:把模型导出到ONNX并用摄像头每秒跑多少帧
4.1 从PyTorch权重到ONNX文件的最小导出
训练完成后,工程上不会直接把.pt文件丢给生产环境,因为PyTorch运行时太重,推理库也不统一。常见做法是先导出成ONNX,再按目标硬件做成CPU上的onnxruntime推理,或者进一步转成TensorRT引擎。下面这段导出代码几乎可以直接抄进你的工程脚本。
# export_onnx.py from ultralytics import YOLO model = YOLO("runs/train/mask_exp/weights/best.pt") # imgsz必须与训练时一致,否则reshape会失效 model.export(format="onnx", imgsz=640, opset=12, simplify=True)导出参数里有两个关键:opset版本和simplify开关。opset=12兼容性最广,尤其是在Windows下用onnxruntime 1.16系列不会遇到算子缺失;simplify=True会剔除一些多余的计算图节点,让模型体积缩小约10%,推理速度也能提升5%。如果后续要上TensorRT,建议导出时也把dynamic保留为默认False,即固定输入尺寸640x640,因为动态尺寸在TensorRT中会显著增加显存占用和预热时间。固定尺寸损失的是多长宽比适应性,但换来的是稳定帧率,这在门禁系统里更有价值。
导出完成后,不要直接用测试集大图去验证,先把一张现场拍的照片缩放到640x640,对比PyTorch原模型的框坐标和onnx模型的框坐标。只要坐标差小于0.01,基本可以认为导出没有破坏精度。这一步能帮你隔离开“模型没训练好”和“部署代码写错了”两类问题,避免后面的排查进入玄学阶段。
4.2 用onnxruntime跑实时摄像头推理
部署端不需要再装深度学习训练框架,只需要onnxruntime和opencv。下面是摄像头推理的核心循环,我已经去掉与业务无关的显示逻辑,只保留最核心的预处理、推理和后处理。
import cv2 import numpy as np import onnxruntime as ort sess = ort.InferenceSession("mask_model.onnx", providers=["CUDAExecutionProvider", "CPUExecutionProvider"]) input_name = sess.get_inputs()[0].name input_shape = sess.get_inputs()[0].shape # [1,3,640,640] def preprocess(frame): img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (640, 640)) # 与导出尺寸一致 img = img.astype(np.float32) / 255.0 # 归一化到0~1 img = np.transpose(img, (2, 0, 1)) return np.expand_dims(img, axis=0).astype(np.float32) cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break inp = preprocess(frame) outputs = sess.run(None, {input_name: inp})[0] # [1,84,8400] # 后处理:解析框、置信度、类别,并映射回原图坐标 # 这里省略NMS代码,生产环境建议直接使用onnxruntime内置的NMS工具或另写向量化NMS cv2.imshow("mask_deploy", frame) if cv2.waitKey(1) & 0xFF == 27: break cap.release() cv2.destroyAllWindows()这段代码里最容易翻车的是输入输出张量的shape。YOLOv8输出的维度是[1, 84, 8400],其中84由4个框坐标加上80个COCO类概率组成。但你自己训练的口罩检测模型只有3类,输出应该是[1, 7, 8400],也就是4个坐标加3个类别概率。很多人直接从网上抄YOLOv8后处理脚本,忘记把类别的索引大小从80改成自己的类别数,结果推理结果一片乱飘。我一般会把输出张量shape打印出来看一眼,这是排查推理问题最快的一步。
参数方面,providers列表顺序决定了推理优先用GPU还是CPU。如果你在Jetson这类设备上部署,建议把TensorrtExecutionProvider放在最前面,其次是CUDA,最后是CPU。但使用CPU设备时,不要图省事把CUDAExecutionProvider也留在列表里,onnxruntime每次初始化都会尝试加载CUDA,失败后再回退CPU,启动时间会平白无故慢三到五秒。对闸机这类需要快速启动的场景,最好在部署脚本里通过命令行参数指定provider,而不是写死。
4.3 帧率与功耗:实时口罩检测的工程红线
实时监控场景有一个硬指标:摄像头采集帧率是25fps或30fps,如果AI推理跟不上,就会出现画面卡顿和漏检。CPU上跑YOLOv8n口罩模型,640x640输入大概在10到15fps,勉强能用但会掉帧;GPU或Jetson Nano上使用四线程推理可以跑到25fps左右。我建议在系统里加一个统计推理耗时的模块,记录最近100帧的平均耗时,当耗时超过40毫秒就自动降低输入分辨率或者跳帧处理。跳帧不是丢帧,而是每隔一帧做一次检测,上一个框坐标保持20毫秒,监控这种场景完全够用。
另一个常被忽略的参数是摄像头分辨率。很多摄像头默认输出1080p,但模型输入只有640x640,这种做法会浪费编码和解码的时间。部署时直接把cv2.VideoCapture的宽高设置成1280x720或960x540,不仅预处理开销降低,摄像头自动曝光也更稳定。口罩检测这类目标不要求超清细节,720p足够。
5. 避坑记录:口罩检测在实战里最常见的六个翻车点
5.1 训练loss一直在0.2上下不动,mAP也一直为零
现象:训练相当流畅,loss快速下降后陷入平台,验证集mAP表现为0或异常低。原因:绝大多数情况是data.yaml类别映射与标注txt中的类别编号错位。例如转换脚本把with_mask放在第0位,但yaml中第0位却写成了without_mask,模型学到的特征和标签完全对调。解决:先打印任意一张训练图片对应的txt标注内容,再用可视化脚本把框画到图上,看框的位置是否和口罩位置匹配。这是所有基于深度学习的图像识别项目通用的第一课,不要绕过。
另一个导致loss不降的原因是目标极小且标注框过小,模型在默认anchor配置下很难匹配到正样本。解决:调低模型输入imgsz到640以下?不对,应该加大imgsz到960,让面部区域在特征图上占更多像素。同时可以降低anchor_t阈值(YOLOv8里对应anchor_t=4.0),让更多候选框参与匹配。
5.2 训练时爆显存,显卡温度飙升
现象:batch设置为32,模型加载后不到10分钟报CUDA out of memory。原因:输入图片分辨率高、batch大、workers多,数据加载进程和训练进程并发抢占显存。解决:先把batch降到8,imgsz降到640,用workers=2,确认能跑通后再逐步增加。如果是云服务器,检查一下nvidia-smi里是否有别人残留的进程占用显存。我遇到过几次“显存不够”实际是被上一个中断的训练进程占着资源,杀掉进程后空间立刻释放。这条排错顺序比调参重要。
5.3 夜间场景下人脸和口罩都偏暗,漏检严重
现象:白天mAP达到0.9,夜间摄像头画面上模型几乎找不到人。原因:训练集里大部分是白天自然光图片,模型依赖亮度颜色特征,而不是形状纹理。解决:从训练数据中抽一部分白天图片,用opencv做亮度随机降低、加噪、模拟夜间红外效果,生成离线增强数据。另一个有效做法是调整摄像头设置,强制开启红外补光并把曝光时间固定在1/50秒,让输入画面亮度保持稳定,比增强模型更省事。
5.4 模型在测试集上很好,一接摄像头就乱框
现象:静态图片测试时检测框稳定,但打开摄像头后,手一挥、门一开,画面里出现大量几十毫秒的闪烁误检。原因:测试集图片是独立的,而摄像头视频流有时间连续性,模型对模糊帧极其敏感;另外人是动态的,运动模糊使脸部特征发生畸变。解决:在后处理中加“连续帧检测抑制”逻辑,只有同一个框连续出现2到3帧才输出告警,单帧结果直接丢弃。这类工程技巧并不会降低模型理论精度,但能让现场误报率大幅下降。
5.5 转换模型后检测框全部偏移到左上角
现象:同一张图,PyTorch推理正常,onnx或OpenVINO推理框全部偏左上角且偏移量固定。原因:这往往是预处理scale方式不一致导致的。PyTorch侧用letterbox,带填充灰边;onnx侧用直接resize,两边把坐标映射到原图时没有去除填充部分。解决:统一两端的预处理逻辑,推荐直接使用ultralytics里封装好的letterbox函数,并在后处理时减去pad的宽度和高度。不要自己手写resize,除非你完全确定边缘填充逻辑一致。
5.6 语音提示模块一直响,闸机却不联动
现象:检测业务逻辑正常,但语音播放或串口命令频繁触发,导致现场体验失控。原因:没有做“检测结果置信度阈值”和“状态机”区分。模型对每个框输出一个概率,如果设置成0.25,可能把远处模糊人脸判成未戴口罩,触发警报。解决:业务层把置信度阈值单独调高到0.4甚至0.5,同时让系统状态在“已提醒/未提醒”之间切换,避免重复触发。这不算模型问题,但部署现场的差评有一半来自这种阈值设置不合理的“活体噪音”。
6. 进阶验证:用测试集切片与特征图确认检测能力,省下“黑匣子”的调试时间
当训练与部署都跑通以后,不要急着写报告或上生产,先做一轮“模型诊断”。我自己的习惯是准备一个约200张的现场切片集,按三种场景归类:正脸近距离、侧脸远距离、逆光带墨镜。对每个切片,分别统计精确率和召回率,然后画混淆矩阵。这一步能帮你明确系统到底是漏检多还是误检多,两个方向对应完全不同的修法。如果漏检集中在远距离,那去调整imgsz和数据增强;如果误检集中在模糊帧,改后处理逻辑;如果类别混淆集中在with_mask和mask_weared_incorrect之间,那基本是标注边界不清晰,需要回到标注阶段统一标准。
另一个值得做的验证是特征图可视化。用PyTorch临时跑一次前向,并把backbone最后一层的feature map叠加到原图上,看模型是不是真的关注了口罩区域。如果发现网络大部分时候盯着背景或额头,说明训练数据分布出了问题。我见过一个案例,模型在教室场景表现很好,但拿到工地后疯狂漏检,特征图显示模型把注意力放到了反光帽檐上,原来公开数据集里大量样本都是戴安全帽的人,模型学到了“帽子”和“口罩”的关联。这种问题只有特征图才能看出来,只看mAP永远发现不了。
训练结束后,还可以对模型做一次鲁棒性测试:把测试图片的分辨率降低到480p、调高锐度、加入高斯噪声,观察mAP下降幅度。遵循“一块边缘盒子的真实算力”去评估,而不是在RTX3090上给出一个漂亮数字。实际上,真正常规部署环境是CPU或嵌入式GPU,所以在导出ONNX后,我会用onnxruntime跑一遍完整的benchmark脚本,输出每一帧的预处理、推理、后处理耗时,记录到日志文件里。这组数字才是你向领导或导师汇报时最有说服力的落地数据。
最后说一个我吃过亏的经验:不要迷信压缩包里自带的README和参数配置,别人能跑通的环境不代表你能跑通。拿训练脚本时,先把data.yaml路径改成自己的绝对路径,再把预训练权重下载好,最后重头跑一遍训练,确保没有任何隐式依赖。好系统的标准不是一次跑出完美模型,而是能让你在拿到数据后快速迭代,还有体面地回滚到上一个版本。这套基于深度学习的口罩检测方案做到测试切片、特征图、耗时记录这三件事后,才算真正从“能跑”变成了“能交付”。希望这些踩坑记录能帮你少熬几个夜,也省下那些起初看似诡异、事后都指向数据或预处理的debug时间。
本文还有配套的精品资源,点击获取