ARTICLE DETAIL

资讯详情

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

基于YOLOV5的口罩佩戴检测:从数据标注到模型部署实战

基于YOLOV5的口罩佩戴检测:从数据标注到模型部署实战 简介基于YOLOV5的口罩佩戴检测项目是一套完整可运行的毕业设计资源面向计算机相关专业正在做毕设的学生以及需要目标检测项目实战练习的学习者。资源整合了数据集、系统源码、训练好的模型与标注数据覆盖从数据准备、模型训练到推理检测的全流程适合课程设计、期末大作业等场景。包内共149个文件压缩包约139.76MB主要包含Python源码与YOLOv5配置py、yaml、训练好的模型权重pt、标注数据与图片jpg、jpeg、txt以及Dockerfile、脚本等辅助内容目录结构清晰便于按模块调用与二次开发。当前已有238人学习下载项目经过严格调试确保能直接运行拿到后即可基于预训练权重开展检测实验或继续优化对理解YOLOv5的训练流程、数据集标注格式和模型部署都有实际参考价值。1. 口罩佩戴检测这个 zip 包数据集、代码、权重和标注是怎么串成闭环的基于 YOLOV5 的口罩佩戴检测是目标检测入门里最常被拿来复现的项目类型之一。你拿到的这个 zip 包如果结构完整里面应该同时包含标注好的图片、YOLO 格式标签、训练/推理代码以及训练好的权重文件——这意味着你不需要从采图和标注开始踩坑而是可以把它当成一张地图先跑通“数据集 → 训练 → 推理”的完整闭环再逐步替换成你自己的业务数据。它能解决工地、园区、学校门岗这类“谁没戴口罩、人在画面哪个位置”的实时定位需求输出的是带边界框的检测结果而不是一句笼统的“戴了/没戴”。适合两类人刚入门目标检测的开发者以及要在两三天内交出一个可演示 demo 的算法工程师。2. 口罩检测为什么默认选 YOLOV5任务边界、模型对比与数据体检2.1 输出“框”而不是“是/否”口罩检测的真实交付物很多新手拿到口罩项目第一反应是“这不就是个二分类吗”。实际交付时你会发现分类模型只能告诉你画面里有没有人没戴口罩但门禁闸机、巡检摄像头、考勤抓拍这些场景需要知道的是“谁没戴、在画面的哪个坐标”。所以口罩佩戴检测本质上是目标检测任务输出每个目标的类别、置信度和边界框bbox后端才能画框、计数、联动报警。类别一般拆成三类而不是两类with_mask正确佩戴、without_mask未佩戴、mask_weared_incorrect戴了但露鼻/挂下巴。第三类的样本量在绝大多数公开数据集里都很少但恰恰是现场最容易引发纠纷的case——巡检人员要的不是“抓到一个没戴的”而是“这个人到底算不算规范佩戴”。训练时如果第三类样本不足模型会把戴歪的全部并到 with_mask 里导致现场漏报。评价指标上这类场景不追求 COCO 那种五十类平均 mAP行业里更看单类 AP0.5 和 FPS。AP0.5 到 0.75 以上在大部分园区和工地场景已经够用低于 0.6 就别急着上线先回去补数据比调参更值。2.2 和 YOLOv7、YOLOv8、SSD 比YOLOV5 赢在交付链路选型这件事纯论指标 YOLOv8 在 COCO 上确实比 v5 高一点但做口罩落地我更推荐 YOLOV5核心原因是交付链路太长。YOLOv5 的生态沉淀最厚onnx 导出、TensorRT 加速、RKNN 量化的现成教程和踩坑记录一搜一大把YOLOv8 的 C2f 模块和 DFL 头在 RK3588 这类边缘盒子上经常遇到算子不支持要手写插件才能跑起来。口罩检测这种目标尺度不大、类别简单、实时性要求高的场景YOLOv5s 的性价比已经足够。和 YOLOv7 比v5 的代码可读性更好报错信息更友好对新手追查问题门槛低。和 SSD 比SSD 的 anchor 设计依赖数据集聚类如果你的图片中人脸尺度分布和公开 VOC 差异很大要先做 anchor 重聚类YOLOv5 提供了 autoanchor训练时会自动计算并更新 anchor省掉一步手动操作。另外 v5 的模型体积也好看v5s 权重约 14MB量化后 7MB 左右放边缘盒子内存压力小。选型上我的建议是手持设备、耗电敏感用 YOLOv5n常规摄像头用 YOLOv5s夜间可见光噪声大换 YOLOv5m不要无脑上 v5x训练时间和推理开销不成比例。2.3 数据体检三类标签、尺度分布与越界框拿到数据集先别急着训练先做一次“体检”。YOLO 格式的标注文件是 txt每行内容是class_id x_center y_center width height其中坐标和宽高全部是相对图片宽高的归一化值范围 0~1。先统计图片分辨率。如果数据集里混着 224、416、640、720 各种尺寸训练时 YOLOv5 会自动 letterbox 统一输入但检测头的感受野和锚框设计是基于某个基准分辨率推算的图片尺寸太杂会轻微影响小脸召回。常见做法是把图片统一到 1280 以内同一档分辨率再开训。再看类别分布。戴/不戴/戴歪三类理想比例在 7:2:1 左右真实数据里“戴歪”往往只有零点几个百分点。如果戴歪样本少于 2000 张这类别在 loss 里的贡献会被淹没模型输出基本全挂在 with_mask 上。遇到这种情况不要硬调损失权重先把戴歪样本补到 2000 张以上再谈训练。最后扫一遍越界框。标签中心点和宽高必须落在 0~1训练前用脚本扫出所有越界值越界的先裁回图片范围、宽高为 0 的直接删掉。这一遍体检虽然枯燥但能避免后面训练时 loss 断崖式变 nan 再回头排查的重复劳动。3. 把标注数据喂给 YOLOV5目录结构、XML 转 TXT 与训练/验证划分3.1 先摸清 zip 里的目录images 与 labels 必须长在同一棵树上打开 zip 包后第一步不是跑训练而是确认目录层级。YOLOV5 训练时依赖一个隐式约定images 目录和 labels 目录同级且内部相对路径一致。比如datasets/mask/images/train/0001.jpg对应的标签必须放在datasets/mask/labels/train/0001.txtv5 的 dataloader 会把同名 jpg 和 txt 自动配对。如果标签散落在别的根目录要在 data.yaml 里分别为 train 和 val 写明images路径和labels路径。用一条命令先把目录树打出来看看find datasets/mask -maxdepth 3 -type d | sort预期输出大致是datasets/mask datasets/mask/images datasets/mask/images/train datasets/mask/images/val datasets/mask/labels datasets/mask/labels/train datasets/mask/labels/val逻辑说明find只看目录层级不关心文件数量-maxdepth 3足够覆盖 train/val 两层。如果发现 labels 下面只有 train 没有 val说明整个包可能没做划分直接跳到 3.3 节补一刀切分。提示图片和标签不要放在不同根目录下也不要出现images/train配labels/val这种错位结构YOLOV5 不会报错但训练时有一半标注对不上mAP 会莫名其妙低。3.2 VOC XML 转 YOLO TXT 的脚本与类别别名映射很多公开口罩数据集的标注是 VOC XML 格式如 FFH 人脸数据集、Kaggle 的 mask detection 数据而 YOLO 训练要的是 txt。转换脚本是这类 zip 项目里最常用的一段代码import xml.etree.ElementTree as ET import os def xml2yolo(xml_path, out_dir, class_map): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_map: # 不在映射表里的类别直接跳过如 person、face continue cls_id class_map[name] bnd obj.find(bndbox) xmin max(0, float(bnd.find(xmin).text)) ymin max(0, float(bnd.find(ymin).text)) xmax min(w-1, float(bnd.find(xmax).text)) ymax min(h-1, float(bnd.find(ymax).text)) bw (xmax - xmin) / w bh (ymax - ymin) / h if bw 0 or bh 0: continue # 宽高为 0 的异常框直接丢 x_center (xmin xmax) / 2 / w y_center (ymin ymax) / 2 / h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}) out_path os.path.join(out_dir, os.path.basename(xml_path).replace(.xml, .txt)) with open(out_path, w) as f: f.write(\n.join(lines))这段代码的逻辑要点有三处。第一类别名要做别名归一化不同数据集里“with_mask”可能叫mask、wearing_mask、with_mask而“没戴”可能叫nomask或without_mask统一映射到同一个 ID 是转换的重中之重。第二越界框不要直接丢弃先裁回图片范围内再判断宽高因为有些标注员会把框画得飞出图片边缘。第三跳过非目标类别后要保证原图有对应 txt 文件哪怕为空文件也会在训练日志里刷 warning但不影响结果。3.3 训练/验证划分脚本与完整性检查如果 zip 包里没给划分好的 train/val或者你想重新划分用下面这个脚本按比例切分import random from pathlib import Path random.seed(42) src_images Path(datasets/mask/images) val_ratio 0.2 ids sorted(list(src_images.glob(*.jpg)) list(src_images.glob(*.png))) random.shuffle(ids) val_id set(p.stem for p in ids[:int(len(ids) * val_ratio)]) for p in ids: split val if p.stem in val_id else train img_dst src_images.parent / images / split / p.name label_src src_images.parent / labels / split / f{p.stem}.txt # 这里省略实际移动/软链接的代码用 shutil.move 或 os.symlink 均可 ...参数说明random.seed(42)固定随机种子保证每次重跑划分结果一致这是复现实验的前提。用p.stem文件名去掉扩展名来匹配图片和标签而不是用完整文件名因为 jpg 和 txt 的扩展名本来就不同。不想复制大文件的话用os.symlink建软链接省磁盘空间但注意打包 zip 时软链接会失效最好直接移动小文件。划分完必须做一次完整性检查统计每个 split 里图片和 txt 的数量差、类别 ID 是否都在 0~2 之间、每类各有多少框。检查脚本会用 data.yaml 的 names 逐类打印数量任何“图片有、标签无”或“标签类别越界”的记录都值得在训练前清理掉。这一步是我见过被跳过的次数最多的步骤也是 zip 包“中看不中用”的第一大原因——标注文件里有脏数据训练时 loss 曲线各种抽风半天查不出原因。4. 训练自己的口罩检测模型环境配置、训练命令与超参数调优4.1 环境版本与显存预算先装对 torch 再谈训练环境配置是第一道坎也是报错重灾区。YOLOV5 官方仓库对 Python 版本要求 3.8PyTorch 建议 1.7 到 2.x 之间。实际操作时最忌“装最新版”因为 torch 版本要和 CUDA 驱动匹配——不匹配时第一次进 GPU 训练就会报CUDA error: no kernel image is available for execution on the device这个报错 95% 的概率是 torch 编译时对应的算力比你显卡新或旧。常见的做法是用 conda 单独建环境conda create -n yolo python3.10 -y conda activate yolo pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple python -c import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.get_device_name(0))逻辑说明requirements.txt是 YOLOV5 仓库自带的依赖清单里面锁了 torch、torchvision、opencv 等的下限版本-i指定镜像源是为了在本地网络环境下加速下载。最后一行 python 命令是装完环境后的“体检”——必须看到True和显卡型号才说明 torch 真的调到了 CUDA。显存预算参考这个表v5s 模型输入分辨率batch size显存占用约可用显卡416326~8 GBRTX 3060 12G6401610~12 GBRTX 3060 / 309064032~20 GBRTX 3090 24G12808~16 GB仅测试不建议训练4.2 一行 data.yaml 决定成败路径、nc 和 names 顺序训练前的核心配置文件是 data.yaml三行关键内容train: datasets/mask/images/train val: datasets/mask/images/val nc: 3 names: [with_mask, without_mask, mask_weared_incorrect]这里有两个坑。第一路径尽量写绝对路径或相对 YOLOV5 仓库根目录的相对路径不要写~/datasets/...这种带 shell 展开符的写法yaml 解析器不会自动展开波浪号。第二names 的顺序必须和标注 txt 里的 class ID 一一对应。ID 为 0 的标签对应 names[0]ID 为 1 对应 names[1]以此类推。如果 yaml 里写反了训练能正常跑完但 val 阶段算 mAP 时会把类别名张冠李戴结果惨不忍睹。预训练权重方面多用yolov5s.pt。口罩类任务和 COCO 的语义差异其实不小——COCO 里有人脸框但没有“口罩佩戴”语义所以不要期待预训练权重能直接给出好结果。常见操作是加--freeze 10冻结前 10 层骨干让模型先专注学习口罩特征而不是重新学底层纹理实测比从零训练收敛更快最终 mAP 通常也更高。4.3 训练命令逐参数拆解从 batch 到 freeze 的落地配置跑通训练的最小命令是这一条python train.py --data datasets/mask/mask.yaml \ --weights yolov5s.pt --img 640 --batch 16 --epochs 100 \ --device 0 --freeze 10 \ --cache ram --seed 42 --name mask_v1参数含义逐个说。--img 640是输入分辨率。数据集原图如果是 1080P用 640 是常见选择原图里人脸小于 24×24 像素的这个分辨率下基本不可能召回需要换更大的 img 或提前裁剪人脸区域。--batch 16先按显存表估算OOM 就降到 8 并配合--accumulate 4做梯度累积等效 batch 还是 32显存占用却小一半。--freeze 10冻结骨干前 10 层。--cache ram把图片缓存进内存能显著缓解磁盘 IO 瓶颈数据集超过 20GB 时改成--cache disk。--seed 42固定随机种子保证两次训练结果可复现——这条后面踩坑章节还会展开。训练日志看什么是关键。打开runs/train/mask_v1/results.csv关注三列train/box_loss、metrics/mAP_0.5、metrics/mAP_0.5:0.95。不要只盯着 loss 曲线loss 下降可能只是过拟合训练集的假象。权重文件在runs/train/mask_v1/weights/下best.pt是验证集 mAP 最优的版本last.pt是最后一个 epoch。部署用 best继续训练用 last。4.4 哪些超参数值得调lr0、fliplr 和 mosaic 都是玄学超参数调优这块我要说句实话第一次训练不要动 hyp 文件用默认的hyp.scratch-low.yaml加上--freeze 10跑出基线再谈调参。口罩任务类别少、背景相对单一调参空间本来就小乱调反而容易过拟合。值得动三个参数。lr0初始学习率如果训练到第 10 个 epoch loss 还在 0.08 以上不降把默认 0.01 调到 0.005 再试如果 loss 前几轮就震荡降到 0.001。fliplr左右翻转增强概率口罩戴在左脸右脸都有默认 0.5 可以无损提升泛化。mosaic马赛克增强我建议数据集小于 1 万张时关掉或降到 0.3——mosaic 会把四张图拼在一起人脸尺度分布被严重扭曲小脸目标反而被带崩。超参数调优没有银弹这是整个训练流程里最接近“玄学”的部分。我的习惯是固定 seed每次只改一个参数重训两三次看 mAP 均值差 0.5 个点以内的都算随机波动别为了一两个点的提升折腾一整天。真正值得花时间的是补戴歪样本比调任何参数都有效。5. 训练和部署避坑5 类让模型翻车的高频问题5.1 loss 直接变 nan先扫标签再怀疑学习率现象训练第一二个 epoch 的 loss 直接输出nan训练无法继续。原因最常见的是标签文件里有越界值。YOLO 格式要求中心点坐标和宽高都在 0~1但原始标注转换时如果没做归一化或转换脚本把像素坐标当相对坐标用就会有几行标签写着x_center3.5这种值。越界标签会让模型在反向传播时计算出的梯度变成 infBN 层参数直接崩掉。解决先写五行的扫描脚本把数据集过一遍from pathlib import Path bad [] for txt in Path(datasets/mask/labels).rglob(*.txt): for line in txt.read_text().strip().splitlines(): parts list(map(float, line.split())) if len(parts) ! 5 or max(parts[1:]) 1 or min(parts[1:]) 0: bad.append((txt.name, line)) break print(bad[:50], bad count:, len(bad))逻辑说明每个标签文件逐行解析检查坐标和宽高是否都落在 0~1。扫出来的越界标签要么删除该框要么回到转换脚本里加max(0, min(coord, 1))的裁剪逻辑。扫完标签确认没问题后再降低学习率重试。记住顺序先查数据再调参数这是排查 nan 的先后铁律。5.2 mAP 全程为 0names 顺序和类别映射错位现象训练 loss 正常下降但 val 阶段 mAP 始终是 0或者某个类别 AP 高、另外两个类别全 0。原因data.yaml 里的 names 顺序和标签 txt 里的 class ID 不一致。比如 txt 里0代表 with_maskyaml 里 names[0] 写的却是 without_mask或者转换脚本里类别映射表写反了把戴帽子的框全部标成 without_mask。解决用训练好的 best.pt 跑一次验证集推理把所有检测结果叠加到图上可视化看框上的类别标签到底对不对。方法是在 detect.py 的--names参数里显式传入自定义 names 文件不要依赖 yaml 推断。如果可视化发现某类明显错位回到 3.2 节的 class_map 映射表逐行检查别名。这条坑的隐蔽性在于训练代码不报错所有指标都在跑只有可视化才能暴露语义错位。5.3 No labels founddata.yaml 路径基准的迷思现象训练启动时报No labels found in ...或AssertionError: train: No labels in ...但目录结构明明是对的。原因data.yaml 里写的路径基准不对。train.py 在启动时会用“当前工作目录 yaml 里的相对路径”去拼接图片和标签路径如果你的相对路径是相对于 yaml 文件位置写的而 train.py 是从仓库根目录运行的拼接结果就会指向不存在的目录。另一类情况是图片路径存在、标签路径写错——dataloader 找不到标签就静默跳过只给一个 warning之后训练出来的模型权重是错的。解决统一用绝对路径或者统一以 YOLOV5 仓库根目录为基准写相对路径。最稳的做法是在 data.yaml 里写死train: /home/user/projects/datasets/mask/images/train改完路径后在终端里手动ls /home/user/projects/datasets/mask/images/train确认目录存在。别嫌这一步简单它避免了启动后才发现“训了十个小时模型全在拟合噪声”的惨剧。5.4 两次训练指标差 3 个点随机种子和数据增强是元凶现象同样的命令、同样的数据集今天训练和明天训练得到的 mAP 差 2~3 个点有时甚至差到 5 个点。原因YOLOV5 的--seed 42只固定了部分随机源PyTorch 的 DataLoader 在开启多进程时worker 内的数据增强mosaic、fliplr、随机 HSV 扰动仍会使用独立的随机序列无法完全复现。另外 mosaic 增强本身会随机裁剪四张图的区域两次训练的增强路径完全不同模型看到的样本分布就不同。解决对比实验时固定--seed还不够要固定三点关闭 mosaic--mosaic 0或在 hyp 里把 mosaic 概率设 0、固定--cache ram保证数据读取顺序一致、固定--workers 8的进程数。即便如此两次训练仍可能有 0.5 个点左右的波动结论性对比请做三次取均值别拿单次结果下结论。这条坑在发论文和做报告时尤其致命——单次跑出 0.78 就声称自己调参有效复现三次可能只有 0.75。5.5 onnx 导出后推理结果全乱后处理与 letterbox 信息丢失现象best.pt 在 PyTorch 里推理正常导出 onnx 后用 onnxruntime 推理画出来的框位置完全错位或者检测出一堆重叠框。原因YOLOV5 的 PyTorch 模型推理时内置了 NMS 后处理和 letterbox 坐标还原导出 onnx 时默认不带 NMS输出是一个[1, 25200, 85]的张量——25200 来自三个尺度特征图80×80 40×40 20×20的锚点总数85 等于 3 个类别 4 个框坐标 80 个类置信度COCO 预训练是 80 类。如果你直接用 argmax 拿类别、不写 NMS、不把检测框坐标换算回原图尺寸结果一定是乱的。解决导出时常见做法是python export.py --weights best.pt --include onnx但在 onnxruntime 里用之前必须自己补上 NMS。另一个高频坑是 letterbox训练时图片被缩放到 640 并填充灰边推理时也必须用同样的 letterbox 方式处理输入图和输出坐标。先后处理、再还原坐标这个顺序不能反。详细的 NMS 代码每家实现略有差异建议先跑通官方 detect.py再逐步替换成自己的后处理不要一上来就重写全部逻辑。6. 把模型用起来CPU 与边缘盒子上的推理和验证技巧6.1 本地 CPU 跑检测onnxruntime 的线程配置如果在没有 GPU 的机器上做演示onnxruntime 是比 PyTorch 更轻量的选择import onnxruntime as ort import numpy as np sess ort.InferenceSession(best.onnx, providers[CPUExecutionProvider]) im np.random.randn(1, 3, 640, 640).astype(np.float32) outs sess.run(None, {sess.get_inputs()[0].name: im}) print(outs[0].shape) # (1, 25200, 85)逻辑说明onnxruntime 的 CPU 推理在 640 输入下大约 80~150ms/帧取决于线程数。providers里如果 GPU 和 CPU 都装了就显式指定 CPU避免自动选择时反复试探。线程数可以设sess_options.intra_op_num_threads默认 4核多的机器调到 8 有明显提升。6.2 边缘设备量化RKNN 校准集要覆盖目标尺度把模型部署到 RK3568/RK3588 这类边缘盒子时量化掉点是常态。常见做法是先用rknn-toolkit2把 onnx 转成 rknn转的时候需要提供校准集图片——这些图片经过定向处理后用于计算每层激活值的量化范围最终生成 int8 权重。校准集的关键要求是“分布覆盖”至少 300~500 张图并且要包含戴歪、逆光、小脸这类难例只用训练集里干净的几百张图做校准集量化后 AP 可能掉 10 个点以上。如果量化后精度掉得厉害先试浮点 rknn 确认精度是否恢复如果浮点精度正常、量化掉点优先检查校准集而不是调量化算法。6.3 现场验证三招置信度、hard case 与时间维度抑制模型部署到现场前我会做三个验证。第一置信度阈值扫描在 0.25~0.5 之间画 AP-THR 曲线挑出一个在“漏检最少、误检可接受”交叉点的阈值不要死守默认 0.25。第二hard case 回测把现场最容易出问题的图片——戴墨镜口罩、口罩上有文字、逆光、侧脸——单独做成一个小测试集逐一跑推理看结果。第三时间维度抑制单帧检测在视频流里会有框闪烁常见做法是加一个“连续 3 帧以上检测到才报警”的计数器能过滤掉大量单帧误检。我现在的习惯是模型训练完先做 5.1 节那种标签体检再做现场 hard case 回测最后才绑定阈值上线。这套流程帮我避开了无数次“训练指标好看、现场一测翻车”的尴尬。希望帮到你。本文还有配套的精品资源点击获取
返回列表