☰
yolov7口罩检测实战:从数据标注到模型部署的避坑指南
2026/10/18 6:37:24 网站建设 项目流程

简介:基于YOLOv7的口罩检测模型资源包,适合需要快速搭建口罩佩戴识别功能的开发人员、安防项目团队及相关专业学生,能判断正常佩戴口罩、未戴口罩和口罩佩戴不规范三类状态,适用于机场、商场、校园等人员密集场所的实时防疫监控。压缩包包含150个文件,整体约669MB,其中权重文件可一键加载,Python脚本负责训练和推理,yaml文件用于模型参数配置,另附标注样例、示例图像、日志记录与说明文档,结构清晰,便于直接验证和改进。已有3049人学习,模型实测精度约93%,并内置多个训练好的权重,附有详细使用教程,读者可结合教程快速集成到监控视频流或智能终端中,减少从零训练的时间成本。资源包中另含训练日志与预测结果图像,可帮助复盘训练过程,整体目录组织规范,适合作为课程设计、毕业设计或企业原型验证的参考素材。

1. yolov7口罩检测:看起来谁都能训,落地却总在翻车

学校门口、工地闸机、商场入口,这类场景里的口罩检测看起来是目标检测的入门题——基于yolov7的口罩检测模型,下载预训练权重、标几千张图、训一晚上,mAP就能到0.95。但真把模型接到摄像头前面,问题才刚开始:五米外的人脸只有二十几个像素;口罩挂在下巴上的违规动作被当成正常;逆光下整张脸黑成一片,模型要么漏检要么乱框。

这篇笔记不打算重复网上那些「一行命令出模型」的教程。我把数据怎么标、模型怎么训、参数怎么调、上线前怎么验收这条链路完整讲一遍,里面的坑大多是我自己翻车翻出来的。适合正在做门禁、考勤、工地安全与口罩联检这类落地项目的工程师和学生。

2. 为什么选yolov7:口罩检测对模型的三个硬要求

2.1 口罩检测不是「分类」,而是「小目标+遮挡+密集」三合一问题

先说为什么常见的人脸检测+口罩分类两阶段方案在落地时容易翻车。第一步人脸检测框如果不够准,截出来的图要么少了额头、要么把背景切进来;第二步的口罩分类器拿到的输入分辨率往往只有几十像素,下半脸又被口罩盖住,分类器只能靠肤色和纹理猜,误判自然多。更麻烦的是,远处的人脸框本身就小,两阶段pipeline会把误差逐级放大。所以这类任务通常直接用单阶段检测模型一次输出「位置+类别」,端到端训练,误差不叠加。

yolov7口罩检测要同时扛住三个压力。小目标:五米外的人脸在640分辨率输入下通常只有20到30像素,模型的多尺度输出头里,负责小目标的浅层特征图得能捕捉到眉眼的弱纹理。遮挡:口罩遮住口鼻,下半脸信息基本失效,模型实际是在靠眉眼、额头和脸型判断,这要求特征提取足够鲁棒。密集:闸机口排队时人与人互相遮挡,NMS参数稍紧就会把相邻目标吃掉。这三个压力决定了一个事实:模型既要对小目标召回好,又要推理够快,还不能在遮挡场景里乱框。

要补一句判断:如果你的场景是「门口单人在两米内刷卡」,那用什么模型都行,麻烦只会出现在距离远、人流密、光线乱的真实环境里。下文所有讨论都以这个现实场景为前提。

2.2 yolov7的几个关键设计在口罩场景里值在哪

yolov7不是横空出世的算法,它是在yolov5基础上改结构、加训练策略的产物。我在口罩数据集上多次对比后,确认它对小目标和遮挡的收益集中在四件事上。

第一是ELAN结构。它把不同层的特征通过更密集的梯度路径串起来,浅层细节和深层语义能更好地融合。对口罩检测来说,浅层特征负责眉眼边缘,深层特征负责「这是一张人脸」的整体判断,两者融合好了,低分辨率人脸才不至于只有轮廓没有证据。第二是辅助训练头。yolov7在训练时额外接一个aux head,用更粗粒度的特征参与loss计算,推理时去掉。它相当于给小目标召回加了一个免费助推,训练成本增加不多,但五米外小人的漏检明显减少。第三是重参数化卷积。训练时的多分支结构在推理时被融合成单路卷积,模型跑起来更快,这对动辄几十路摄像头并发推理的现场很重要。第四是autoanchor机制,它会根据你的标注框重新聚类anchor,比手工猜anchor靠谱,尤其适合人脸这种宽高比相对固定的目标。

不过要冷静一点:这些设计解决的是「同样的数据下多榨几个点」的问题,不是「数据很烂也能训好」的问题。口罩检测的精度瓶颈通常不在模型结构,而在标注口径和场景分布,这一点后面两章会反复强调。

2.3 选型对比:yolov7比yolov5和yolov8好在哪、差在哪

如果你正在这几个常被问到的模型之间做选择,我按口罩检测场景画了一张对比表,不是跑分表,是落地感受:

模型精度与速度特点部署生态适合场景
yolov7速度和精度平衡,小目标略强TensorRT资料多,后处理坑有现成答案已有yolov5经验、需要提点提速的团队
yolov5稳,社区排错贴最多老设备兼容最好快速出demo、团队不熟yolov7
yolov8API友好,内置增强多新框架,部分旧芯片部署资料少原型验证多、部署环境新的项目

我的默认选择是yolov7。原因很实际:口罩检测是二分类小模型,yolov7和yolov5在最终mAP上差距通常不超过一个点,但yolov7的aux head让远距离小人漏检更少;yolov8虽然训练代码更优雅,但很多老相机、老工控机环境里,别人踩过yolov7的TensorRT坑,你遇到的概率就小。做落地项目,选「坑有人踩过」的比选「指标好看」的更稳。

什么时候别用yolov7?如果你的设备是树莓派级别的内存和算力,用yolov7-tiny;如果项目要用最新的训练API快速试错且部署环境全在新平台上,yolov8更省事;如果公司已经有完整的yolov5前后端链路,别为了零点几个点迁移。选型是成本问题,不是面子问题。

2.4 先跑通一次推理:确认环境再决定要不要投入

在开始标数据之前,我强烈建议先用仓库自带的预训练权重把流程跑一遍。这一步不产出口罩检测效果,目的是确认环境、显存、依赖没问题,免得标了两千张图才发现训练起不来。

git clone <yolov7官方仓库地址> cd yolov7 pip install -r requirements.txt python detect.py --weights yolov7.pt --source test.jpg --img-size 640

这个detect.py会用COCO上预训练的yolov7权重对一张普通图片做检测,输出到runs/detect目录。如果你看到画面里person被正确框出来,说明依赖装好了、模型能跑;如果报错,先看是不是权重没下载完整或者版本不匹配。等后面训练出best.pt,把命令里的--weights换成best.pt,就能直接检测口罩。

注意这里的detect.py逻辑:它对每张图先做letterbox缩放,再做一次前向推理,最后做NMS,然后把框画回原图。口罩场景里如果输入图宽高比很极端,letterbox产生的灰边会占掉一部分分辨率,实际有效分辨率比640更低,这就是为什么很多人在超宽监控画面上感觉模型变笨了。这个问题后面讲部署时还会碰到。

3. 数据准备:标注规范、格式转换与难例挖掘

3.1 标注口径:两类还是三类,先想清楚再动手

我在好几个项目里吃过标注口径不一致的亏,所以先把这一节放在最前面。口罩检测最常见的类别设计有两种:两类(with_mask / without_mask),或者三类(加一个incorrect,表示口罩没戴好)。两类方案简单,标注快,模型也好收敛;三类方案能直接识别「口罩挂在下巴」「口罩遮住眼睛」这类违规动作,但incorrect和without_mask在视觉上经常只差几厘米,标注员很容易把同一张图标出不同类别,模型会在这两个类之间反复摇摆。

我的经验是首版先做两类,把口罩不在口鼻位置的样本统一归入without_mask。为什么?因为你要检测的本质是「这个人的口罩戴没戴好」,而不是给口罩状态做精细分类。先保证without_mask的召回,上线后如果业务方确实需要区分「没戴」和「戴一半」,再补第三类重训,而不是一开始就把标注一致性搞崩。

标注框的规范也要写进给标注团队的文档里:框住人脸主体,不要包含太多头发和肩部;口罩遮住脸时按可见轮廓标注;目标很密集、互相遮挡时也要逐个标注,不能因为「看不清」跳过——漏标比标错更难排查,模型会把漏标的对象学到背景里去。

3.2 从VOC XML到YOLO txt:转换脚本与四个边界坑

多数标注工具导出的是VOC格式的XML,而yolov7训练需要的是每张图一个同名txt,每行「类别id cx cy w h」的归一化坐标。转换脚本不复杂,但边界坑不少,下面这个脚本是我常用版本的浓缩:

import xml.etree.ElementTree as ET CLS_NAMES = {"with_mask": 0, "without_mask": 1} def convert_voc(xml_path, img_w, img_h, out_txt_path): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter("object"): name = obj.find("name").text if name not in CLS_NAMES: continue box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) cx = ((xmin + xmax) / 2) / img_w cy = ((ymin + ymax) / 2) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h cx = min(max(cx, 0.0), 1.0) cy = min(max(cy, 0.0), 1.0) w = min(max(w, 0.0), 1.0) h = min(max(h, 0.0), 1.0) if w < 0.001 or h < 0.001: continue lines.append(f"{CLS_NAMES[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(out_txt_path, "w", encoding="utf-8") as f: f.write("\n".join(lines))

这个脚本的逻辑很直白:解析XML里的每个object,把左上右下角坐标换成归一化的中心点和宽高,再按yolov7要求的格式写进txt。传入的img_w和img_h是原图尺寸,必须和XML里size字段一致,否则归一化坐标会整体偏移。这里四个坑要特别说明。

第一,类别id一定要从0开始,和后面配置文件里的names顺序对死,否则模型训练时类别错位,mAP再高都是假的。第二,clip操作会把贴边框的中心点拉向图像内部,如果目标一半在画面外,clip后的框其实是错的——这类图应该人工复核或者直接不标,别指望脚本替你纠正。第三,宽高小于0.001的框说明标注质量极差或者目标极小,保留它会污染box_loss,直接丢弃并打印日志方便追溯。第四,读写都指定utf-8编码,Windows环境下XML里带中文路径时,默认编码处理会直接报错。

3.3 难例挖掘:把「戴歪的口罩」喂进训练

口罩检测最容易漏的图往往不是标准正脸,而是逆光、侧脸、低头、口罩挂在下巴、手正好挡住嘴这些日常动作。训练数据里如果没有足够的这种样本,模型学到的就是「正中光线下完整人脸」的强特征,一换环境就失灵。

难例来源最靠谱的是目标场景实地录视频再抽帧。我的做法是:在每个点位录30分钟视频,按每5秒抽一帧,抽完人工过一遍,把模糊帧和重复帧去掉,剩下的大概率覆盖了逆光、人流密度变化、各种姿态。注意抽帧要按时间段分布,不要集中在同一个小时的连续画面,否则又是一批同源数据。

数据增强方面,yolov7的mosaic默认是开的,四张图拼一张,小目标数量变多变密,对提升远距离人脸召回很有帮助。但如果你的loss在训练中后期一直震荡,可以试试把mosaic关掉或降到0.5——它不是越多越好,拼图产生的伪影在二分类任务上有时弊大于利。mixup同理。口罩检测的瓶颈大多是数据分布问题而不是数据量问题,增强只是辅助手段。

3.4 数据划分与类别平衡:先统计再训练

划分数据集时最容易犯的错是随机划分。同一段视频相邻帧之间高度相似,随机划分会把同一批人的前后帧同时分进train和val,验证集失真,训练时模型相当于提前看到了验证对象。正确做法是按「视频片段」或「摄像头」划分,一个片段的帧全部进train或全部进val,这样验证集才是真正没见过的画面。

训练前我还会跑一个类别统计脚本,看看正负样本比例。代码很简单,就是遍历label目录统计每个类别出现次数:

import os from collections import Counter counts = Counter() for split in ["train", "val"]: label_dir = f"datasets/mask/labels/{split}" for filename in os.listdir(label_dir): with open(os.path.join(label_dir, filename), encoding="utf-8") as f: for line in f: cls = line.split()[0] counts[(split, cls)] += 1 print(counts)

这个脚本会输出每个split下每个类别的标注框总数,比如train的with_mask有8000个、without_mask有2000个。如果两类样本比例超过1:5,我会先补少数类数据,而不是急着在loss上加权重。数据侧解决类别失衡比调参可靠,因为你增加的样本同时也在丰富场景多样性。比例在1:1.5到1:2之间时不用处理,这个任务天然允许一定偏斜。

4. 训练口罩检测模型:目录、配置与参数

4.1 目录结构:先建好能复现的数据仓库

yolov7训练时靠的是「图片路径+同名txt标签」这套隐式对应关系,目录结构不规范,后面所有脚本都会跟着乱。我习惯这样组织:

mkdir -p datasets/mask/images/train mkdir -p datasets/mask/images/val mkdir -p datasets/mask/labels/train mkdir -p datasets/mask/labels/val

图片放images下,同名txt放labels下,文件名除了扩展名外必须完全一致。比如images/train/00001.jpg对应labels/train/00001.txt。训练前我还会跑一条bash命令,检查有没有图片没标签、或者标签没图片的孤儿文件:

for f in datasets/mask/labels/train/*.txt; do base="${f%.txt}" [ -f "$base.jpg" ] || echo "missing image: $base.jpg" done

这个检查很重要。标签和图片对不上是最隐蔽的训练事故之一,它不会让你训练报错,只会让模型在某个类上悄悄变笨,事后极难排查。把这条命令也跑一遍val目录,两边都干净再进下一步。

4.2 修改配置:五个必改项

第一步是写数据配置,我一般放在data/mask.yaml里:

train: datasets/mask/images/train val: datasets/mask/images/val nc: 2 names: ['with_mask', 'without_mask']

train和val写相对路径,注意是从yolov7仓库根目录出发的路径,别写成绝对路径,否则换机器训练还要改配置。nc是类别数,names的顺序要和转换脚本里CLS_NAMES的id完全一致。

第二步是改模型配置。训练用的模型配置文件在cfg/training/yolov7.yaml,注意是training目录下的那个,不是cfg/deploy下的——deploy版本没有aux head,直接拿它训练会少一个关键的小目标辅助分支,很多人在这里翻车。只需要把文件里的nc改成2,其他结构参数不要动。

注意:训练用cfg/training/yolov7.yaml,推理导出用cfg/deploy/yolov7.yaml,二者不能混用。

第三步是anchor。yolov7在训练时会根据你的标注框自动重新聚类anchor,所以手动改anchor通常没必要。除非你的目标尺寸分布极其特殊,否则先让autoanchor自己跑。第四步是超参数。hyp.scratch.yaml里我常调的是mosaic和mixup,前面说过loss后期震荡时把mosaic降到0.5试试;学习率lr0默认0.01,如果batch-size比较小(8以下),我会降到0.005,防止梯度更新太激进。第五步是输入分辨率。img-size默认是640,如果场景人脸普遍很小,可以试736或768,但速度会明显下降。通常640起步,训练完用难例集验证再决定要不要加。

4.3 训练命令与参数调法

配置改完就可以启动了。下面是我在口罩数据集上最常用的一条命令:

python train.py \ --weights yolov7.pt \ --data data/mask.yaml \ --cfg cfg/training/yolov7.yaml \ --epochs 120 \ --batch-size 16 \ --img 640 \ --workers 8 \ --device 0 \ --cache-images

参数逐个说清楚。--weights用COCO预训练权重yolov7.pt,迁移学习起点必须是它,从随机初始化训练二分类模型既慢精度又差;--data和--cfg就是上面改好的两个文件;--epochs我一般先给120,口罩数据集规模不大,60轮左右通常就收敛了,多出来的轮数靠best.pt机制兜底;--batch-size 16在24G显存的卡上没问题,8G卡降到4到8,显存不够先降batch而不是降分辨率;--img 640是速度和精度的折中;--workers 8是数据加载线程数,Windows下如果卡在加载阶段,把它改成0;--device指定GPU;--cache-images把训练图一次性缓存进内存,小数据集能明显加快每轮迭代,缺点是吃内存,数据集超过两万张就不要开。

训练日志和权重默认输出到runs/train/exp目录,每次运行自动递增exp1、exp2。我习惯启动后用nvidia-smi看一眼显存占用,确认没有OOM风险再离开。如果OOM,把batch-size砍半重跑,不要先动分辨率。

4.4 训练中看什么:loss与mAP怎么配合

训练开始后,最值得盯的文件是runs/train/exp*/results.png,它画了每个epoch的box_loss、obj_loss、cls_loss和mAP曲线。口罩是个二分类任务,cls_loss通常很快收敛成一条平线,不用管;重点看box_loss和obj_loss是否稳定下降,以及mAP@0.5是否持续抬升。

如果P(精确率)很高但R(召回率)一直上不去,说明漏检集中在难例上,大概率是数据问题而不是训练问题,去补难例数据比继续加epoch有用。如果mAP@0.5连续15到20个epoch不增长,我就手动停掉,用已经保存的best.pt。yolov7默认保存两份权重:best.pt和last.pt,best.pt是验证集mAP最高的那一版,last.pt是最后一轮,最后用best.pt。

训练中途断掉也不用从头再来:

python train.py --resume runs/train/exp/ --data data/mask.yaml

注意resume时保持原来的数据配置和目录不变,改了路径会加载不到状态。也别把--weights再填一遍,resume逻辑会自己读last.pt。恢复后先确认日志里的epoch数接得上,再接再训。

5. 训练与落地避坑:五条血泪经验

5.1 口罩挂下巴被当成「第三种状态」漏掉

现象:测试视频里,有人把口罩拉到下巴或脖子上,模型不输出框,或者输出的置信度只有0.1出头,而业务恰恰要求这种「没戴好」也要报警。

原因:标注口径里把这类样本归入了without_mask,但数量太少。with_mask和without_mask的视觉差异主要是「口罩位置」,模型学到的是「下半脸有没有被遮住」这个强特征。挂下巴时下半脸暴露,按逻辑应该判成without_mask,但样本少,模型根本没把「下巴位置的口罩」和「没戴口罩」关联起来,最后表现出来就是干脆不输出。

解决:把「口罩不在口鼻位置」的样本全部归入without_mask,并针对这个难例做定向补数据。我实测过把挂下巴样本从二十来张补到两百张后,这个难例上的召回涨了约五个百分点。补数据比调阈值有效得多,因为本质是特征分布没覆盖到。同时,类别的判定规则要写进标注文档,训练前抽查10%样本的标注一致性,别让标注员按自己的感觉自由发挥。

5.2 验证集与训练集同源,mAP虚高

现象:训练时mAP@0.5到0.95,同一批摄像头抽帧的验证集也是0.94,但模型部署到一个没参与过训练的新摄像头,mAP掉到0.75左右。

原因:随机划分把同一段视频的前后帧同时分进train和val,模型实则靠背景纹理和画面连贯性在认人,而不是靠人脸本身。验证集和训练集越像,mAP就越不可信。判断方法很简单:如果train loss和val loss差距小到异常,而且验证集里几乎每张图都跟训练集某几张图「长得像」,大概率就是同源了。

解决:按视频片段或摄像头划分数据集,一个片段的帧全部进train或全部进val。另外,无论数据怎么划分,都要单独留一组跨场景难例集,永远不参与训练和验证,只在每次训练后测试。这组数据才是你对外承诺精度时真正要看的依据。

5.3 loss曲线正常但mAP震荡,像是玄学

现象:box_loss和obj_loss稳定下降,但mAP@0.5在0.85到0.95之间来回跳,同一份数据重训一次,最后的结果还不一样。

原因:验证集太小,统计噪声大,几百张图里多漏一张脸,指标就掉一个点;mosaic和mixup每轮生成不同的拼接图,等于每轮数据分布都在变;学习率策略在收敛末期不够平滑,导致权重在最优解附近来回摆动。

解决:先把验证集扩到500张以上,这是成本最低的手段;然后固定随机种子,保证每次实验可比;再把lr0从0.01降到0.005,或者把hyp里的lr_scheduler改成cos,让末尾段学习率平滑下降。另外,yolov7默认开了EMA(指数滑动平均),best.pt是带EMA的权重,不要拿last.pt直接上生产,那版权重通常更颠。我遇到这类问题通常先改验证集规模,因为最容易发现是「模型没训好」还是「指标本身在跳」。

5.4 新场景漏检:光线、角度、分辨率三个变量

现象:室内灯光下准确率很高,一到户外逆光就漏检;平视摄像头没问题,从下往上拍的仰角画面漏检严重。

原因:训练数据里逆光样本和仰角样本太少,模型对亮度通道和姿态分布非常敏感。口罩目标本身特征就弱,光线或角度一变,眉眼轮廓的纹理被高光或阴影压掉,模型就认不出来。

解决:到目标场景实地录视频补样本,覆盖逆光、侧光、仰角、低头四种最常翻车的条件。逆光样本选在早上和傍晚两个时段各录一段,仰角样本用低位安装的摄像头拍。抽帧时分散在不同时间段,避免集中在同一小时内的连续帧,否则补进来的又是不平衡的同源样本。补完再跑一遍难例集,看这几类漏检有没有针对性改善。

5.5 显存不够就降分辨率,结果小目标全丢

现象:8G显卡跑640分辨率OOM,把--img降到416就能训,模型也能收敛,但一测远端小人,全漏检。

原因:分辨率从640降到416,输入面积缩小了近60%,原本20到30像素的人脸变成12到18像素,小目标特征几乎消失,数据增强也救不回来。这不是模型变差了,是输入信息本身不够。

解决:优先降batch-size,其次开--cache-images减少IO压力,再不行换yolov7-tiny从头训。降分辨率是最后手段,而且要在降完之后用难例集重新验证一遍,而不是只看mAP没降就放心。如果必须用416跑,输入端的裁切策略也要跟着改,比如超宽监控画面先做目标区域裁切,而不是整幅图缩进去。

6. 用「难例集」验收模型:比盯着mAP靠谱的固定动作

6.1 建一个不进训练集的难例集

在项目目录旁边放一个hard文件夹,选20到30张真正让模型难受的图:逆光、密集排队、口罩挂下巴、五米外的小人、仰角视角。这些图只用来验收,永远不进训练集。我每次换数据、改参数、调超参之后都要跑一遍。

python detect.py --weights runs/train/exp/weights/best.pt \ --source hard/ --conf-thres 0.2 --img-size 640

detect.py会遍历hard目录,把带框结果保存到runs/detect目录。--conf-thres设0.2比线上常用的0.25更严苛,逼着模型把低置信度目标也框出来,这样你能看到它是「漏了」还是「框了但置信度低」,两种情况处理方式完全不同。漏了去补数据,置信度低则先调阈值或看是不是输入分辨率不够。

6.2 养成先跑难例集再看指标的习惯

我现在训完模型的第一件事不是看mAP,而是拿最新best.pt跑一遍这20来张难例图,和上一次训练的输出逐张对比。漏检数变多了,先查数据或参数变动,而不是急着部署。我自己靠这个习惯抓住过好几次「mAP没掉但难例漏检悄悄变多」的回归问题。

阈值选择上也给一个实际经验:口罩检测场景里漏检的代价通常比误检高,比如没拦住一个没戴口罩的人,远比把海报人脸误报一次严重。线上阈值我一般设在0.2到0.25;如果误报太多,常见于海报、玻璃反光上的脸,就往上调并增加目标跟踪去重,不做单帧硬判。mAP是统计数字,难例图上漏了就是漏了,骗不了人,这个习惯帮我挡掉了好几次翻车。希望帮到你。

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

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

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

立即咨询