简介一份基于深度学习的口罩检测系统完整工程包面向高校毕业设计、课程设计与目标检测入门者提供可运行的YOLO检测方案与配套数据组织。系统包含数据标注、训练、验证到批量检测的完整流程并配有网络结构配置、类别标注文件和测试图片能帮助快速复现口罩佩戴识别场景也便于替换自定义数据集开展实验。工程中脚本覆盖模型训练、格式转换、锚点聚类等关键环节结构文件定义检测网络文本文件存放数据集划分、类别与先验框信息图片提供直观测试素材整体共45个文件压缩包仅2.64MB以脚本、文本与模型配置类文件为主另含少量图片、字体与编译缓存目录分类清晰便于按需查阅。目前已有43人学习使用。整套资源既适合用于期末课程设计或毕业设计的基础模板也能帮助初学者了解目标检测从数据准备到模型推理的完整落地流程并在此之上扩展新的识别场景。1. 为什么“口罩检测系统”值得自己从头搭一遍一个zip包背后的完整技术栈从网上下载一个“基于深度学习的口罩检测系统.zip”解压后通常是几百MB的权重文件加一段推理脚本——但这恰恰是最容易翻车的地方直接跑起来能检测换一批数据就失灵。做口罩检测难点从来不在“能检”而在数据分布、模型选型和部署环境的匹配。我一般按下面这套流程走先把数据集解压、切分、增强再决定用分类还是检测网络训完把模型封装成带摄像头推理的zip部署包最后用mAP和FPS两张表判断能不能上线。这套路径适合刚入门深度学习的开发者也适合要在园区闸机、教室监控里落地口罩识别的工程人员。读完你能照着复现一套自己的系统而不是停留在解压一个黑匣子的阶段。2. 数据集与预处理从zip到训练样本先解决“有没有标注框”的问题2.1 公开数据集选型图像级标签和框级标注决定了你能训出什么模型做口罩检测第一个要分清的不是选哪个模型而是标签形式。公开数据集大致分三类图像级标签告诉你“这张图里有人戴了口罩”框级标签告诉你“这张图里哪个人脸在哪个位置、戴没戴”。前者训出来只能做整图判断后者才能支撑闸机、教室这种需要定位的多人场景。我见过不少人拿图像级数据直接训YOLO标注文件转出来全是空框这就是任务定义没对齐。数据集标签形式适合做什么我一般怎么用MaskedFace-Net图像级戴口罩/不戴二分类做分类基线MAFA框级遮挡多困难样本补充和主力数据混合Kaggle Face Mask Detection框级检测模型主力按8:1:1切分训练MaskedFace-Net是贴图生成的正负样本干净但缺少真实遮挡适合验证分类思路MAFA里的人脸大量被口罩、墨镜甚至围巾遮挡难度高把它按小比例混进训练集能明显提升困难场景的鲁棒性Kaggle上的Face Mask Detection是框级标注训练YOLO通常拿它当主力。要注意网盘里传的zip来源不一解压前先核对文件hash解压后还要检查一遍标签有没有空框、错位。如果手里只有图像级数据常见补救做法是先用分类网络对视频抽帧打伪标签再人工抽检把明显错误的帧删掉保留90%以上置信度的样本然后转换成框级数据训练检测模型。伪标签质量不到90%不要直接训检测不然模型会学到一堆错误位置先验。标签文件格式也要统一YOLO系用的是归一化的txt一行一个目标class_id x_center y_center width height。转换脚本常见的坑是把像素坐标直接除以图像宽高却忘了减去左上角偏移框超出图像边界的样本要裁掉或跳过否则训练时会报“invalid box”错误。2.2 从解压到标准格式切分、标签转换与数据增强全流程先说解压。zipfile.extractall在Python 3.11之前不校验成员路径恶意压缩包能把文件写到上级目录这就是常说的“zip滑解压”。老版本代码要手动检查每个member.name拒绝包含“..”的条目3.11及以上可以用filter参数限制。未知来源的zip也不要用系统解压工具直接双击先跑一遍7z t或python -m zipfile -t验证完整性这一步能省掉后面大半“解压失败”的血泪教训。import zipfile from pathlib import Path import numpy as np # 1) 解压原始zip。filter 参数只在 Python 3.11 可用 # 老版本要手动判断 member 里有没有“..”再决定是否解压。 with zipfile.ZipFile(mask_dataset.zip, r) as zf: zf.extractall(data/raw, filterlambda m: not m.is_dir()) # 2) 找到所有 jpg/png按 8:1:1 随机切分 paths list(Path(data/raw).rglob(*.jpg)) rng np.random.default_rng(2024) rng.shuffle(paths) train, val, test np.split( paths, [int(len(paths) * 0.8), int(len(paths) * 0.9)], ) print(ftrain{len(train)} val{len(val)} test{len(test)})代码里的np.split第二个参数是0.8、0.9两个切分点分别得到80%、10%、10%三份第三个切分点传0.9而不是1.0否则会多出一份空数组。固定随机种子2024保证每次切分结果一致实验才能对比。如果你的总样本数不到10008:1:1偏激进我一般改成7:1.5:1.5并配合K折交叉验证不然验证集太小指标忽高忽低完全没法判断模型好坏。接下来是标签转换。很多人拿到的原始数据是VOC的xml或CSV直接喂YOLO会报错需要先统一成YOLO的txt格式。import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, out_dir, class_ids): tree ET.parse(xml_path) root tree.getroot() w int(root.find(size/width).text) h int(root.find(size/height).text) lines [] for obj in root.findall(object): name obj.find(name).text b obj.find(bndbox) x1, y1, x2, y2 [float(b.find(k).text) for k in (xmin, ymin, xmax, ymax)] # 边界框超出图像的部分要裁掉否则YOLO训练报 invalid box x1, y1 max(0, x1), max(0, y1) x2, y2 min(w, x2), min(h, y2) if x2 x1 or y2 y1: continue xc (x1 x2) / 2 / w yc (y1 y2) / 2 / h bw (x2 - x1) / w bh (y2 - y1) / h lines.append(f{class_ids[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}) out out_dir / (xml_path.stem .txt) out.write_text(\n.join(lines))转换时一定要先裁掉超出边界的部分再做归一化不然中心点和宽高的值会大于1训练时解码出来的框跑到画面外损失直接震荡。class_ids是字典比如{with_mask: 0, without_mask: 1}顺序要和最终训练配置保持一致。数据增强是预处理里最值得花时间的部分。下面这个增强函数会在训练循环里被调用每次迭代生成不同的样本。import cv2 import numpy as np def augment(img, boxes): # 水平翻转口罩左右不对称的样本很少翻转是最划算的增强 if np.random.rand() 0.5: img cv2.flip(img, 1) if boxes is not None and len(boxes): w img.shape[1] boxes[:, [0, 2]] w - boxes[:, [0, 2]] # 镜面映射 boxes[:, [0, 2]] boxes[:, [2, 0]] # 交换 x1/x2 # 亮度扰动只改 HSV 的 V 通道避免色相偏移导致口罩颜色失真 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) v hsv[:, :, 2].astype(np.int16) v np.clip(v np.random.randint(-30, 30), 0, 255) hsv[:, :, 2] v.astype(np.uint8) img cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR) return img, boxes增强有两个要点。第一水平翻转后必须同步改框坐标先做镜面映射再用交换x1/x2的方式修正大小关系这个顺序错了框就乱套。第二亮度扰动范围(-30,30)是经验值暗光场景多可以放到(-50,50)。注意在训练循环里做增强不要预先增强后存盘那样会多占几十倍存储空间而且每个epoch见到的样本完全一样增强等于白做。增强做完看一眼类别分布。如果你解压后发现“不戴口罩”样本是“戴口罩”的两倍先别急着训练。比例超过1.5倍时最常见做法是在数据加载时给少数类复制两份或者用加权采样器直接硬训的话模型会被多数类带着走后果在5.1里详细说。预处理阶段解决类别问题比训练中调损失函数省心得多。3. 模型选型与训练分类网络还是检测网络损失函数怎么配3.1 检测任务为什么默认走YOLO路线从MobileNet到检测头模型选型的本质是任务边界。分类网络输出一个概率检测网络输出N组“框坐标类别置信度”。口罩检测落到闸机、教室现场要回答的是“哪个人没戴口罩”这就锁定了检测网络。如果摄像头固定拍单人那分类网络也能应付但那种场景叫门禁小功能不叫系统不在这次讨论范围。检测网络的常见基线是YOLO系列YOLOv8n整个模型才3M参数级CPU也能推。MobileNetV3可以作为轻量backbone但自己拼检测头、损失函数和NMS的成本不小常见做法是直接用Ultralytics YOLO。检测头原理不复杂在特征图上预置锚点对每个锚点回归偏移量并预测目标概率卷积神经网络CNN靠嘴鼻区域的细节区分“戴”和“没戴”所以输入尺寸不能压太小640×640是个平衡点。也有人说用SSD或Faster R-CNN。SSD在密集小目标上不如YOLO干净Faster R-CNN精度高但两阶段结构在CPU上跑不动实时。口罩检测目标只有两类、人脸尺寸比较一致YOLO系的一阶段方案在精度和延迟之间最划算。选型表如下方案输出参数量级适合场景MobileNetV3分类整图0/12~5M单人单摄像头YOLOv8n检测框类别置信度约3M多人闸机/监控YOLOv5s检测框类别置信度约7M精度要求更高表里的“约”是有意写的。实际参数量随输入和类别数浮动不要只盯着论文里的数字。选型时真正要对比的是两个数字同精度下的FPS和同FPS下的mAP。YOLOv8n在两者之间最均衡这也是为什么它在口罩检测这类任务里被用得最多。3.2 用迁移学习训YOLOv8n冻结层、学习率与早停的搭配数据集准备好后第一步不是写模型结构而是写数据配置。下面是mask.yaml的最小内容。path用相对路径YOLO会自动拼到当前工程目录下names的顺序必须固定训练和推理阶段要完全一致顺序换了导出模型后类别就全错了。# mask.yamlYOLO 训练需要的最小数据配置 path: data/mask train: images/train val: images/val nc: 2 names: [with_mask, without_mask]训练脚本用PyTorch生态的Ultralytics接口迁移学习是标准做法。COCO预训练权重里有人脸但没口罩类别我们只需要把检测头换成两个类主干特征直接继承。from ultralytics import YOLO # 用 COCO 预训练权重起步比随机初始化快且稳 model YOLO(yolov8n.pt) # mask.yaml 里 nc2 会把检测头换成两个类主干权重保留 model.train( datamask.yaml, epochs50, imgsz640, batch16, lr00.01, freeze10, patience10, projectruns/mask, nameexp1, )freeze10是把前10层参数固定住。前几层学到的是边缘、纹理这类通用特征预训练权重已经足够好在小数据集上微调浅层反而会把通用特征冲掉。经验是数据量几千张时冻结前10层数据量上万后可以不冻结。loss震荡的时候先把lr0从0.01降到0.005不要一上来就0.001迁移学习里学习率太低反而收敛到差的局部最优点。batch16按8GB显存估显存不够优先降batch到8不要忍痛把imgsz降到320人脸口罩细节会丢。patience10的意思是验证损失连续10轮不降就早停强制跑满50轮在小数据集上几乎必然过拟合。训练完后看runs/mask/exp1/weights/best.pt是按验证损失选的不是最后一轮。跑测试集用model.val(datamask.yaml, splittest)这一步才是模型真实水平的体现。4. 把模型封装成系统推理脚本、摄像头流与zip部署包的结构4.1 用OpenCV接摄像头做实时推理conf阈值和画框的最佳实践训练完成之后真正的工程从推理脚本开始。常见做法是加载best.pt接摄像头循环读帧每帧送入模型把结果画回画面。下面这个脚本是能直接跑起来的最小版本我把阈值、设备选择都放在参数里部署时改一处就行。import cv2 from ultralytics import YOLO model YOLO(runs/mask/exp1/weights/best.pt) cap cv2.VideoCapture(0) # 0 是默认摄像头 while True: ok, frame cap.read() if not ok: break results model.predict(frame, imgsz640, conf0.45, device0) for r in results: boxes r.boxes for b in boxes: x1, y1, x2, y2 map(int, b.xyxy[0].tolist()) cls_id int(b.cls[0]) label mask if cls_id 0 else no_mask cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f{label} {b.conf[0]:.2f}, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(mask detection, frame) if cv2.waitKey(1) 27: # ESC 退出 break cap.release() cv2.destroyAllWindows()代码里device0写死GPU在只有CPU的机器上会直接报CUDA错。更稳的写法是运行时判断device0 if torch.cuda.is_available() else cpu。conf0.45是经验值——想统计漏检率就调低到0.3宁可多画几个框再人工复核想减少前台误报就调到0.5以上。没有通用最优阈值每版模型都要在真实场景里重新标一次。逐帧predict会反复做预处理做系统时把模型初始化放循环外是基本要求。如果你的摄像头每秒30帧而推理只有10帧可以先丢帧用cap.read()按需读把画面队列长度压到1否则延迟会越来越大。waitKey(1)里的1毫秒也别改成0改成0之后图像窗口不刷新很多人被这个坑卡半天。4.2 打成zip部署包目录结构、requirements和三个路径坑部署包的结构我一般保持下面这个样子。权重、源码、依赖清单和说明分开避免把训练日志和数据一起打包。项目里所有路径都写成相对路径代码里用Path(file).resolve().parent.parent定位项目根目录不然换个目录就找不到权重。mask_system/ ├── weights/ │ └── best.pt ├── src/ │ ├── detect.py │ └── config.yaml ├── requirements.txt └── README.md打包命令也很直接关键是用-x排除runs和data这两个目录动辄几个G不排除的话zip会膨胀到没法传。zip -r mask_system.zip mask_system \ -x mask_system/runs/* -x mask_system/data/*三个路径坑要记住第一不要写绝对路径用相对定位第二requirements.txt锁版本常见做法是把ultralytics锁到8.x系列、opencv-python锁到4.9系列不锁版本的话两周后依赖升级可能让模型输出全部变成NaN第三压缩包内所有文件名统一用英文Windows上中文文件名解压后乱码是zip分发最常见的翻车现场没有之一。环境配置也一样写进部署包。常见做法是附一份environment.yml部署时conda env create -f environment.yml一步建环境比在一台裸服务器上逐个pip装省太多事。如果部署目标机器不能联网那就要把torch、ultralytics的whl文件一起放进zip。模型权重不要用带密码的zip分发获取成本每高一点使用者就少一半。真要防止模型被白嫖建议转ONNX再部署或走服务端API而不是给压缩包加密码。5. 口罩检测的常见坑与排查精度低、误检多、内存爆的3类问题5.1 漏检和误检的根源多半在数据不在模型类别失衡与暗光场景现象一训练时loss一路下降验证时召回率却上不去戴口罩的人被漏检。原因类别不平衡。当“不戴口罩”样本是“戴口罩”的两倍以上交叉熵损失被多数类主导模型学到的判定边界偏向输出no_mask。解决在数据层面处理最简单。先统计两类样本数比例超过1.5倍时把少数类在加载时复制两份或者用加权采样器给少数类更高的采样概率。数据层面解决不了再动损失YOLO里给分类损失乘类权重也是一种做法但参数调起来更玄学我一般先试数据。排查时先跑model.val(splitval)看每一类的召回率如果without_mask的召回率高、with_mask的召回率低直接看训练集类别直方图不用怀疑模型结构。现象二白天一切正常一到夜间路灯下漏检率翻倍逆光时还把戴了口罩的人识别成没戴。原因训练数据大多是白天室内拍的亮度分布和部署现场差太远。这是数据分布偏差问题模型泛化能力再强也填不上这个沟。解决推理前加一步CLAHE自适应直方图均衡三行代码能把夜间细节拉回来一大截同时训练阶段的增强里加入亮度扰动和gamma校正。注意CLAHE的clipLimit设2.0、tileGridSize设8×8这两个值在720p画面下是经验起点。夜间场景多就再专门录一批夜视频补进训练集比调任何超参数都有效。排查时挑10张暗光失败样本逐张看如果人眼都看不清那是现场摄像头帧质量差需要前端补光或换相机如果人眼看得清但模型检不出才是数据分布问题走CLAHE和补数据的路线。这两类问题定位有个共性别看测试集整体指标看每一类的recall。mAP被多数类拉高后单类指标才会暴露问题。Ultralytics训练完会在results.csv里输出每个类别的precision、recall、mAP50先把without_mask那行的recall找出来低于0.9就要回头查数据。5.2 部署现场的三类“翻车”CPU掉帧、zip损坏与密码zip现象三CPU推理只有2-3 FPS画面一顿一顿根本没法实时。原因模型选大了或者还在用PyTorch fp32跑640输入。YOLOv8s在CPU上的代价比n级大很多fp32在CPU上也不是最优计算格式。解决换成YOLOv8nimgsz降到416导出ONNX后用onnxruntime推理。下面是ONNX推理的预处理入口注意letterbox保持宽高比不要直接resize拉伸人脸变形后框会偏。import cv2 import onnxruntime as ort import numpy as np sess ort.InferenceSession(best.onnx, providers[CPUExecutionProvider]) def preprocess(img): # letterbox 保持宽高比避免人脸变形 h, w img.shape[:2] scale 416 / max(h, w) nh, nw int(h * scale), int(w * scale) img cv2.resize(img, (nw, nh)) canvas np.full((416, 416, 3), 114, dtypenp.uint8) canvas[:nh, :nw] img return canvas, scale这段代码只做了预处理模型输出还要做decode和NMS。ONNX导出的raw output在nc2时是[1,7,8400]形状8400是三个尺度特征图上的anchor总数74个框坐标1个objectness2个类别概率。CPU上YOLOv8n加onnxruntime在主流i5上大概10-15帧低于5帧先查是不是输入尺寸没降到416再查代码里是不是在循环里初始化了模型。现象四从网盘下载的zip解压时报“could not find EOCD”或者“invalid zip archive: could not find eocd”。原因EOCD是zip尾部结束记录找不到它绝大多数情况是文件没下完或传输过程被截断少部分是下载工具把zip当文本处理过。解决先看文件大小跟下载页是不是一致差几KB就是截断然后7z t mask_system.zip测一遍完整性。如果提示“unsupported compression method”说明压缩方用了比较新的算法升级7-zip到最新版。带密码的zip没有捷径正规做法是找分发方要密码网上那些“zip密码移除”脚本顶多对ZipCrypto弱加密有效AES-256加密的zip基本没后悔药别浪费时间。注意部署阶段的“能跑”和“能上线”是两回事。CPU上2FPS能验证逻辑但现场要的是稳定帧率验收清单在下一章给。6. 验证与进阶用mAP和FPS两张表判断系统值不值得上线6.1 验收指标别只盯mAPrecall和每类AP才是上线标准mAP0.5是通用验收指标但口罩检测更该盯漏检率——把没戴口罩的人放进去才是事故。mAP高但漏检率高的模型不能上线。我一般会录三段真实视频白天、夜间、逆光逐帧手工标框后跑推理统计漏检率。下表是一组合格的验收示例数值为示意不同场景差异很大场景mAP0.5漏检率CPU FPS结论室内白天0.932%12可上线夜间路灯0.7418%12加CLAHE后再测强逆光0.5535%12不建议直接上线FPS这条线要按现场定。闸机口10帧以上才能出实时效果5帧以下只能做离线分析。指标合在一起看别只看单张表。mAP高但FPS低只能说明模型精度好跑不动等于白搭。6.2 一个具体技巧模型导出ONNX再转TensorRTFPS翻倍最有性价比的优化路径是导出ONNX再转TensorRT或OpenVINO。导出命令一行# 导出 ONNXhalfTrue 把权重转 fp16 model.export(formatonnx, imgsz640, halfTrue, dynamicFalse)拿到best.onnx后用trtexec生成enginetrtexec --onnxbest.onnx --fp16 --saveEnginebest.engine。注意导出时imgsz定640运行时letterbox也要按640输入尺寸不一致engine会直接拒绝。dynamicTrue在开发时方便但生产上固定尺寸的engine少一步绑定FPS更高。另外TensorRT的engine和显卡型号绑定换机器要重新生成这个要在部署文档里写清楚。我最早做这个系统时只盯mAP模型在校验集上0.95上线三天后被一盏路灯打回原形逆光误检率直接翻倍。后来养成的习惯是每版模型先过一遍“白天夜间逆光”三段录像再谈部署。这个习惯帮我避开了大部分坑希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?