YOLOv7口罩检测实战:从数据标注到部署避坑全指南
2026/9/24 22:39:54 网站建设 项目流程

简介:基于YOLOv7的口罩检测模型项目包,面向需要快速落地口罩识别场景的开发者与算法工程师。模型可同时检测戴口罩、未戴口罩和佩戴不规范三类目标,在验证集上检测精度约百分之九十三。资源包含一百五十个文件,压缩包约六百七十兆;其中pt权重文件可直接替换推理,yaml与py为配置和训练推理脚本,jpg与png为样例和可视化结果,另有xml、pdf、md等说明文档,目录结构清晰。已有三千零四十五人学习下载。项目提供多个训练好的模型和详细使用教程,可加载模型对图像、视频流进行实时检测;同时涵盖数据预处理、Darknet架构搭建、损失函数调优、精度评估等完整训练流程,便于二次训练与部署,适合用于公共场所防疫监控、园区安全巡检及智能硬件集成等场景。对希望深入理解YOLOv7单阶段检测原理的学习者,也能从配置文件和训练日志中获得直观参考。

1. 口罩检测为什么选 YOLOv7:别急着追新,先看算力账

入户门禁、园区闸机、工地安全帽加口罩的双重检查,这些场景里口罩检测模型早就不是实验室玩具,而是直接跑在边缘盒子或本地 GPU 服务器上的生产代码。YOLOv7 虽然已经是两年前的模型,但到今天做口罩检测,我依然会先推荐它,原因是它在精度和推理速度之间给的余量最大:COCO 上 41.2% 的 mAP 对口罩这类单类目标绰绰有余,而同等精度下它的参数量和计算量比 YOLOv8 更低,对 Jetson、瑞芯微这类边缘设备更友好。这篇笔记想跟你讲清楚一件事:用 YOLOv7 做口罩检测,从数据标注格式、训练命令到部署避坑,完整走一遍,哪些参数是玄学、哪些是真坑,我踩过的你就不用再踩了。

2. 环境与数据准备:从标注格式到目录结构一次理顺

2.1 依赖版本怎么锁:Python、PyTorch、CUDA 的三角关系

YOLOv7 官方仓库是基于 PyTorch 实现的,对版本不像后来 YOLOv8 那么严格,但也不能完全不锁。我的习惯是先固定 Python 3.8 或 3.10,PyTorch 选 1.13 或 2.0。这里有个实际问题:PyTorch 2.0 的编译机制(torch.compile)和 YOLOv7 原仓库有兼容问题,如果你直接pip install torch==2.0然后跑训练,大概率会遇到类型推断报错。建议直接用 1.13.1,省心。

CUDA 版本和显卡驱动要匹配,我一般用 CUDA 11.7 搭配 PyTorch 1.13.1,这个组合在 30 系和 40 系显卡上都很稳定。

conda create -n yolov7-mask python=3.8 conda activate yolov7-mask pip install torch==1.13.1+cu117 torchvision==0.14.1+cu117 --extra-index-url https://download.pytorch.org/whl/cu117 git clone https://github.com/WongKinYiu/yolov7.git cd yolov7 pip install -r requirements.txt

这段代码先建一个独立 conda 环境,避免把系统 Python 搞乱。之后装指定版本的 PyTorch,这里--extra-index-url指向 PyTorch 官方源,不是默认 PyPI,版本号必须和 CUDA 版本一一对应,写错一个数字就会在导入 torch 时报 「CUDA not available」。最后克隆 YOLOv7 官方仓库并安装依赖。requirements.txt 里主要是 opencv-python、numpy、matplotlib 这些,装完就具备跑通训练的最基本条件了。

注意:如果你用的是 40 系显卡,CUDA 11.7 也能用,但建议装 CUDA 11.8 的 PyTorch 版本,性能稍微好一点。另外,Windows 下别用 conda 装 opencv,容易和系统自带 DLL 冲突,pip 直接装更稳。

2.2 口罩数据集怎么组织:VOC 格式转 YOLO 格式的脚本与边界坑

口罩检测数据集的标注格式主要有两种:VOC 格式(XML 文件)和 YOLO 格式(TXT 文件)。网上开源数据多为 VOC 或 JSON(COCO),而 YOLOv7 训练需要 YOLO 格式,即每张图片对应一个同名 TXT,每一行是类别 x_center y_center width height,坐标是归一化到 0~1 的浮点数。

我先说目录结构,这是新手第一关。YOLOv7 的数据集路径由 data 文件里的 yaml 指定,但目录本身长这样:

mask-dataset/ ├── images/ │ ├── train/ │ │ ├── mask_001.jpg │ │ └── ... │ └── val/ │ ├── mask_002.jpg │ └── ... └── labels/ ├── train/ │ ├── mask_001.txt │ └── ... └── val/ └── mask_002.txt

images 和 labels 是兄弟目录,不是父子关系,这一点搞反了训练时就会报找不到 label。每个 TXT 文件内容类似这样:

0 0.5234375 0.44140625 0.278125 0.36197917

这里的 0 是类别编号——我习惯把「戴口罩」设为 0,「未戴口罩」设为 1;四个数字依次是中心点 x、中心点 y、宽、高,全部是相对图片宽高的比例。

如果你拿到的是 VOC 的 XML 标注,通常做法是写一段转换脚本:

import xml.etree.ElementTree as ET import os def convert_voc_to_yolo(xml_path, out_dir, classes): 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) txt_name = os.path.splitext(os.path.basename(xml_path))[0] + '.txt' with open(os.path.join(out_dir, txt_name), 'w') as f: for obj in root.iter('object'): cls_name = obj.find('name').text if cls_name not in classes: continue cls_id = classes.index(cls_name) 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) x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 边界检查:防止坐标越界 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) if w < 0.001 or h < 0.001: continue # 过滤掉过小标注 f.write(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}\n") classes = ['with_mask', 'without_mask'] # 使用示例:遍历所有XML文件 for xml_file in os.listdir('voc_annotations'): if xml_file.endswith('.xml'): convert_voc_to_yolo( os.path.join('voc_annotations', xml_file), 'yolo_labels', classes )

这段脚本逻辑不复杂,但有几个边界坑值得说:第一,bbox 坐标必须做归一化,否则训练时 loss 会变成 NaN,这是最常见的翻车原因。第二,bbox 的值不能越界,如果标注框本身超出了图片边界(标注工具里常见),要做 clip 操作。第三,过滤掉宽或高小于 0.001 的微小框,这类框通常是误标注,留着会让 loss 在训练初期剧烈抖动。

转换成 YOLO 格式后,还要检查一下有没有出现空 TXT 文件。如果某张图里所有标注都被过滤了,YOLOv7 在训练时会跳过这张图,但会打印警告。

3. 训练 YOLOv7 口罩模型:参数、命令与调优路径

3.1 修改 data yaml 和模型 yaml:nc 必须和数据集对齐

环境装好了、数据整理好了,下一步是配置训练入口。YOLOv7 训练需要两个 yaml:数据配置和模型配置。数据配置放在data/目录下,自己建一个mask.yaml

train: ../mask-dataset/images/train val: ../mask-dataset/images/val nc: 2 names: ['with_mask', 'without_mask']

这里最关键的是nc必须和你标注里的类别数一致。如果你只检测「有没有戴口罩」,但想区分「戴了」和「没戴」两种状态,nc 就是 2;如果只检测「没戴口罩」这一个违规目标,nc 就是 1。类别编号从 0 开始,names 顺序要和标注文件里的数字一一对应,不然训练出来的模型推理结果张冠李戴。

模型配置文件用官方自带的cfg/training/yolov7.yaml,你不用改,但要会看几个关键字段。里面的nc: 80是 COCO 预训练模型的类别数,YOLOv7 的代码在加载预训练权重时会自动把最后一层卷积的通道数按你数据集的 nc 重新初始化,所以不必手动改模型 yaml 里的 nc。你只需要在训练命令里通过--cfg指定这个文件,代码会处理。

3.2 最小可跑训练命令与关键超参数说明

我一般用预训练权重做迁移学习,而不是从零训练。用 COCO 上训练好的yolov7_training.pt做初始权重,收敛速度快的不是一点半点,口罩检测和 COCO 的语义空间有一定重叠(COCO 有人、有帽子),这些预训练的底层特征对口罩检测非常有用。

python train.py \ --workers 4 \ --device 0 \ --batch-size 16 \ --data data/mask.yaml \ --img 640 640 \ --cfg cfg/training/yolov7.yaml \ --weights 'yolov7_training.pt' \ --epochs 80 \ --hyp data/hyp.scratch.p5.yaml \ --name mask-exp1

先解读参数:--workers 4是数据加载进程数,Windows 下建议设为 0,否则会报BrokenPipeError,Linux 下可以开到 4 或 8。--batch-size 16在 8GB 显存的 GPU(比如 RTX 3070)上差不多是上限,如果显存溢出,降低到 8 即可。--img 640 640是训练输入分辨率,这个值影响精度和速度的平衡,后面单独说。--hyp指定超参数文件,--name是实验名,输出保存在runs/train/下。

这段命令背后的逻辑链是:YOLOv7 读入 mask.yaml 确认数据路径和类别数,读入 yolov7.yaml 构建网络结构,再读入预训练权重,但最后一层检测头的类别相关参数会被重置。你可以观察日志,第一次迭代时会打印Model Summary: 414 layers, 37028038 parameters这类信息,如果参数数量和官方不一致,说明权重加载出了问题。

训练过程中的关键观察点有两个。第一是iou_lossobj_loss这两个 loss 值,正常情况应该平稳下降。第二是显存占用,如果出现 CUDA out of memory,优先降 batch size 而不是换小模型。

3.3 超参数调整:学习率、图像分辨率和 anchors 的取舍

YOLOv7 默认超参数文件hyp.scratch.p5.yaml里的初始学习率是 0.01,用 SGD 优化器配合余弦退火。对口罩检测这种相对简单的任务,这个学习率偏激进。我习惯把它改小三分之一到lr0: 0.007,虽然收敛变慢,但稳定性好很多,不容易在训练早期 loss 炸掉。

另一个值得调的是--img分辨率。口罩检测的目标相对大(人脸占画面比例高),用 640 输入就够;如果你识别的是远处人群中的口罩,目标很小,可以考虑 1280 分辨率训练,但显存占用是 640 的 4 倍,推理速度也慢不少。实战中先 640 起步,看 mAP 不够再上 1024,别一上来就追求高分辨率。

关于 anchors,经常有人问要不要重新算。我的结论是:用 COCO 预训练权重迁移学习时,不要动 anchors,保持模型 yaml 里的默认值即可。COCO 的 anchors 覆盖面广,已经能适应口罩检测的目标尺度分布。只有你从零开始训练模型时才需要考虑用 k-means 重新聚类 anchors 适配你的数据集。

超参调整我没有用过多的自动搜索工具(如 Optuna),口罩检测的调参空间没那么大,手动试两三轮就基本摸到底了。真正的坑不在超参,而在数据的分布和标注质量。

4. 部署推理:从 PyTorch 权重到可用的检测服务

4.1 导出 ONNX 与 TensorRT:格式转换的三种路径对比

训练完成后,你得到的是runs/train/mask-exp1/weights/best.pt,这是一个 PyTorch 权重文件。直接拿它做部署不是不行,但 PyTorch 的推理有额外依赖开销、启动慢、显存占用高,对生产环境不友好。常见的做法是转成 ONNX 再在推理框架里跑。

YOLOv7 官方仓库自带了export.py,一条命令导出 ONNX:

python export.py \ --weights runs/train/mask-exp1/weights/best.pt \ --img-size 640 640 \ --batch-size 1 \ --end2end \ --simplify \ --topk-all 100 \ --iou-thres 0.65 \ --conf-thres 0.25

其中--end2end是把 NMS 也集成到模型输出里,导出的模型直接输出最终的检测框;如果不加这个参数,模型输出的是数千个预选框,需要在外部做 NMS 后处理。--simplify用 ONNX Simplifier 做图优化,能砍掉一些冗余算子,让推理框架兼容性更好。导出后的best.onnx可以直接用 ONNX Runtime 加载。

三种部署路径对比见下表:

路径推理速度集成难度适用场景
PyTorch 原生最慢快速验证、调试
ONNX Runtime CPU中等无 GPU 的 Linux 服务器、x86 工控机
TensorRT FP16最快中高Jetson、嵌入式 GPU、生产级视频流

TensorRT 的转换需要先从 ONNX 转 engine 文件,NVIDIA 官方工具trtexec或者 Python API 都行,而且 TensorRT 版本和显卡驱动要配套,这一块内容够单独写一篇。简单给一条命令参考:

trtexec --onnx=best.onnx --saveEngine=best.engine --fp16 --workspace=2048

--fp16启用半精度推理,速度基本翻倍,口罩检测这种任务精度损失可以忽略。需要注意 TensorRT 的 engine 文件是绑显卡架构的,在 3090 上转的 engine 不能拿到 4090 上直接用,部署到新机器上时必须重新转换。

4.2 用 ONNX Runtime 写一条最小推理流水线

核心部署代码用 ONNX Runtime 实现,不依赖 PyTorch,环境瞬间变轻:

import onnxruntime as ort import cv2 import numpy as np class MaskDetector: def __init__(self, onnx_path, conf_thres=0.5, iou_thres=0.45): self.session = ort.InferenceSession(onnx_path, providers=['CUDAExecutionProvider', 'CPUExecutionProvider']) self.conf_thres = conf_thres self.iou_thres = iou_thres # 获取输入输出信息 self.input_name = self.session.get_inputs()[0].name self.input_shape = self.session.get_inputs()[0].shape def preprocess(self, img): # 保持长宽比的 letterbox 缩放 h, w = img.shape[:2] target_size = 640 scale = min(target_size / w, target_size / h) new_w, new_h = int(w * scale), int(h * scale) resized = cv2.resize(img, (new_w, new_h)) canvas = np.full((target_size, target_size, 3), 114, dtype=np.uint8) canvas[:new_h, :new_w] = resized # BGR -> RGB, HWC -> CHW, 归一化 blob = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0 blob = np.transpose(blob, (2, 0, 1)) return blob, scale, new_w, new_h def postprocess(self, outputs, scale, new_w, new_h, orig_shape): # outputs 形状: [1, num_dets, 6] 或 [1, num_dets, 4+num_class] dets = outputs[0][0] # 取第一张图 results = [] for det in dets: if len(det) < 6: continue x1, y1, x2, y2, score, cls_id = det[:6] if score < self.conf_thres: continue # 坐标还原到原图尺寸 x1 = int(x1 / scale) y1 = int(y1 / scale) x2 = int(x2 / scale) y2 = int(y2 / scale) results.append((x1, y1, x2, y2, score, int(cls_id))) return results def main(): detector = MaskDetector('best.onnx', conf_thres=0.5) cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break blob, scale, nw, nh = detector.preprocess(frame) outputs = detector.session.run(None, {detector.input_name: [blob]})[0] detections = detector.postprocess(outputs, scale, nw, nh, frame.shape) for x1, y1, x2, y2, score, cls_id in detections: label = f'with_mask: {score:.2f}' if cls_id == 0 else f'without_mask: {score:.2f}' color = (0, 255, 0) if cls_id == 0 else (0, 0, 255) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imshow('mask-detection', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows() if __name__ == '__main__': main()

这段代码整体分三步。preprocess做 letterbox 缩放,把任意分辨率的图像统一到 640×640,同时记录缩放比例和填充尺寸,这一步必不可少——推理完成后需要把检测框坐标映射回原图。postprocess解析 ONNX 输出,注意不同导出方式输出维度不同:--end2end导出的模型输出是[1, num_dets, 6],每行是x1, y1, x2, y2, score, class;不加--end2end的模型输出是裸预测框,需要在后处理里自己写 NMS。providers列表指定了优先使用 GPU 推理,没有 GPU 的环境会自动回退到 CPU,这保证了代码的兼容性。

ONNX Runtime 的 CUDA 执行提供程序(CUDAExecutionProvider)需要单独安装对应版本的 onnxruntime-gpu 包,如果直接用pip install onnxruntime,只能用 CPU 推理。两个包不能共存于同一环境,需要干净的环境单独装。

5. 避坑与常见问题:口罩检测训练部署中的 5 个典型故障排查

5.1 现象:训练 loss 不降反升,甚至直接变 NaN

原因:最常见的是数据集里有损坏的图片或标注文件,其次是学习率设置过大,还有一种是标签类别编号超出nc范围。

解决:先把--epochs改成 1 跑一遍,看能否走完一个 epoch;如果卡在某个 batch 上,逐个图片验证哪张是坏的;用try-except包裹数据加载流程打印出错路径。学习率方面,把hyp.scratch.p5.yaml里的lr0从 0.01 降到 0.005,同时warmup_epochs保持 3.0 不动。标注问题检查方式:

python -c " with open('labels/train/000001.txt') as f: for line in f: parts = line.split() cls = int(parts[0]) coords = [float(x) for x in parts[1:]] assert 0 <= cls <= 1, 'class id error' assert all(0 <= c <= 1 for c in coords), 'coords out of range' print('OK') "

5.2 现象:训练集 mAP 接近 1,验证集 mAP 只有 0.6 左右

原因:这是典型的过拟合,口罩数据集规模普遍偏小,几千张图对于 YOLOv7 的参数体量来说远远不够。

解决:第一选择是加强数据增强。YOLOv7 默认的hyp.scratch.p5.yamlhsv_hhsv_shsv_v是颜色增强参数,degrees是旋转角度。我把默认的旋转从 0 改成 10(正负 10 度),scale从 0.5 改成 0.8,translate从 0.1 改成 0.2,让模型见过更多姿态的人脸。另外正负样本比例如果是 1:5 这种严重倾斜的,要用加权采样或者重复采样少数类。

5.3 现象:torch.load 加载权重时报错或者缺少 key

原因:权重文件路径不对、预训练权重和你克隆的仓库版本不匹配。

解决:YOLOv7 官方 releases 里提供yolov7_training.ptyolov7.pt两个预训练模型。yolov7.pt是完整 COCO 模型,yolov7_training.pt是专门用于迁移学习的版本——它把检测头部分做了特殊处理,加载时不会报 key 不匹配。如果报错,先确认你下的是yolov7_training.pt,然后检查代码仓库是否更新到了最新 commit。有些第三方 fork 会修改模型结构,导致权重 key 对不上。

5.4 现象:推理时检测框偏移严重,位置对不上

原因:推理脚本里的 letterbox 缩放和训练时不一致,或者后处理没有做坐标映射。

解决:训练时的预处理逻辑在datasets.py里,推理脚本里的 letterbox 必须跟它保持完全一致——包括填充颜色 114、缩放的插值方式(默认cv2.INTER_LINEAR)。我用过一个省心办法:直接 import 仓库里的letterbox函数而不是自己重写:

from utils.plots import plot_one_box from utils.datasets import letterbox

但注意 deployments 里用 ONNX Runtime 推理时,你不想引入整个仓库依赖,这时就照着 4.2 节的 preprocess 方法自己写,确保填充值、归一化范围(除以 255)完全一致。

5.5 现象:batch-size 设为 16 时报 CUDA out of memory

原因:显存不够,但 YOLOv7 的默认配置里有多尺度训练(每个 10 个 batch 会随机变换输入分辨率),这会临时占掉更多显存。

解决:把 batch size 降到 8,或者在训练命令里加--noautoanchor --nosave之外,还可以直接修改train.py里的--multi-scale默认值。如果你用 6GB 显存尝试跑 640 分辨率的 YOLOv7,压力会非常大,此时把--img降到 416,batch 降到 4,损失一部分精度但能跑起来。另外 Windows 下注意把--workers 0,否则数据预加载也会挤占系统内存并拖慢训练。

6. 从「能跑」到「可靠」:验证你的模型真的能用

模型训练完、推理跑通,这只是第一步。从「在验证集上 mAP 好看」到「现场真的不出事」,中间还隔着一段需要认真对待的距离。

我常用的一个验证技巧是把测试集按场景分组,分别计算每个子集的 mAP。口罩检测的数据通常混杂了室内、室外、强光、逆光、戴眼镜、戴帽子这些因素,你把它们拆开单独看,往往会发现模型在某个子集上的表现远差于整体均值。比如整体 mAP 0.89,但逆光场景只有 0.72——这说明模型记住的是亮度特征而非口罩本身。发现这个问题后,我给逆光图片加了亮度扰动和直方图均衡增强,重新训练一版,整体 mAP 没怎么变,但逆光子集从 0.72 提到了 0.83。这个提升不体现在总分数上,却直接决定了现场漏报率。

另外一个建议是给模型加一个「异常拒判」机制。ONNX 输出每个框都有置信度,现场部署时把阈值从默认的 0.25 提到 0.5,确实会漏掉一些低置信度的正确检测,但能大幅减少误报。如果是闸机这种安全敏感场景,可以开两条检测路径:一帧低阈值、一帧高阈值,两次结果做投票。这个方案的延迟大概增加 30%,但稳定性好很多,值得试。

最后的习惯是每次训练完都固化三样东西:训练命令、data yaml、超参数文件。同一个模型,三个月后你大概率想不起来当初用了什么分辨率、什么增强策略,YOLOv7 原仓库没有自动记录这些的习惯,自己手动存一份跑不了亏。我每次训练完会在runs/train/目录下留一个train_args.txt,把命令原文贴进去,下次复现完全不用猜。这比任何实验管理工具都好使,至少对你个人来说是零维护成本。

希望这次的实战拆解能帮你少走些弯路,祝你的口罩检测模型一次训练就达到上线标准。

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

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

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

立即咨询