ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

YOLOv7口罩检测实战:从数据标注到部署避坑全指南

YOLOv7口罩检测实战:从数据标注到部署避坑全指南 简介基于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.10PyTorch 选 1.13 或 2.0。这里有个实际问题PyTorch 2.0 的编译机制torch.compile和 YOLOv7 原仓库有兼容问题如果你直接pip install torch2.0然后跑训练大概率会遇到类型推断报错。建议直接用 1.13.1省心。CUDA 版本和显卡驱动要匹配我一般用 CUDA 11.7 搭配 PyTorch 1.13.1这个组合在 30 系和 40 系显卡上都很稳定。conda create -n yolov7-mask python3.8 conda activate yolov7-mask pip install torch1.13.1cu117 torchvision0.14.1cu117 --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 或 JSONCOCO而 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.txtimages 和 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 和模型 yamlnc 必须和数据集对齐环境装好了、数据整理好了下一步是配置训练入口。YOLOv7 训练需要两个 yaml数据配置和模型配置。数据配置放在data/目录下自己建一个mask.yamltrain: ../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否则会报BrokenPipeErrorLinux 下可以开到 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_loss和obj_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一条命令导出 ONNXpython 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 --onnxbest.onnx --saveEnginebest.engine --fp16 --workspace2048--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_thres0.5, iou_thres0.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, dtypenp.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, 4num_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_thres0.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 fwith_mask: {score:.2f} if cls_id 0 else fwithout_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.yaml里hsv_h、hsv_s、hsv_v是颜色增强参数degrees是旋转角度。我把默认的旋转从 0 改成 10正负 10 度scale从 0.5 改成 0.8translate从 0.1 改成 0.2让模型见过更多姿态的人脸。另外正负样本比例如果是 1:5 这种严重倾斜的要用加权采样或者重复采样少数类。5.3 现象torch.load 加载权重时报错或者缺少 key原因权重文件路径不对、预训练权重和你克隆的仓库版本不匹配。解决YOLOv7 官方 releases 里提供yolov7_training.pt和yolov7.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降到 416batch 降到 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把命令原文贴进去下次复现完全不用猜。这比任何实验管理工具都好使至少对你个人来说是零维护成本。希望这次的实战拆解能帮你少走些弯路祝你的口罩检测模型一次训练就达到上线标准。本文还有配套的精品资源点击获取
返回列表