ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLOv4+PyTorch人脸口罩识别:从环境搭建到NMS后处理全流程实战

YOLOv4+PyTorch人脸口罩识别:从环境搭建到NMS后处理全流程实战 简介这套基于YOLOv4和PyTorch的人脸口罩识别项目源码完整包含Python训练/推理代码、模型权重、VOC格式标注数据、字体文件、说明文档和多段测试视频覆盖从数据准备到模型部署的主要环节。资源共367个文件其中322个xml标注文件为训练集提供框选坐标20个py脚本负责数据加载、模型训练与检测推理另有pyc缓存、txt配置、jpg样例图和avi输出视频等压缩包整体仅9.31MB轻量易用。目前已有52人学习下载。资源适用于计算机、人工智能、自动化等专业学生的毕设、课程设计或实践入门代码经过运行验证作者标注答辩均分96分可靠性较高。配合README和演示视频可快速复现口罩佩戴检测效果理解YOLOv4的改进结构、PyTorch损失计算与推理流程也能基于标注数据自行扩充训练集做迁移学习或二次开发。1. 基于 YOLOv4 与 PyTorch 的人脸口罩识别工程包里真正要打通的是哪四段医院闸机、工地板房、校园考勤过去两年这些场景批量上线过一种判断人脸识别之前先判断有没有戴口罩。这类需求看起来简单落地时却比想象中多绕好几道弯——仓库里给的是 darknet 的 .weightsPyTorch 加载第一步就报 shape mismatch数据标注格式跟 cfg 里的 classes 对不上训练到一半 loss 发散视频测试时框一帧有一帧无。标题这套“源代码文档数据模型测试视频”表面是一个工程包实质是一条完整闭环PyTorch 环境、数据标注、改 cfg、训练、导入 darknet 权重、推理加 NMS、最后拿测试视频验收。这篇文章只讲这条闭环里的关键选择和可复现参数适合毕设、竞赛也适合一周内需要交付验证的安防集成项目。2. YOLOv4 和 PyTorch 的选型理由与基础环境搭建口罩是典型的小尺度目标半张脸的宽度往往只有几十个像素还伴随遮挡、逆光、低头这类噪声。YOLOv4 在整个 YOLO 家族里不是最新最强的但它的结构复杂度、训练技巧公开度以及 darknet 权重到 PyTorch 的转换路径都足够成熟做业务验证比直接上 YOLOv8 更省试错成本。这一章先把架构上它靠什么解决口罩目标的问题讲清楚再落到 PyTorch 安装、版本匹配和工程目录因为后续训练与推理都建立在 PyTorch 基础框架上。2.1 口罩目标为什么适合用 YOLOv4 的检测头设计YOLOv4 相比 v3 的三个改动恰好打在口罩识别的痛点上。第一是主干 CSPDarknet53把特征图分成两部分再合并减少重复梯度网络加深的同时训练更稳在口罩这种“目标小、背景杂”的数据上训练稳定比网络参数多更重要。第二是 PANet 路径聚合把 52×52 浅层的位置信息和纹理传到高层输出YOLOv4 三个尺度分别在 13×13、26×26、52×52 上检测大中小目标半张脸小口罩主要落在 52×52 分支。很多人小口罩检不出来不是因为模型不够强而是输入分辨率只有 320小目标在图中只剩十几个像素调大输入到 608 后 mAP 会有明显变化。第三是 Mish 激活和 DIoU-NMS。Mish 在负值区间更平滑对口罩边缘这类高频梯度波动响应更温和训练初期不容易震荡DIoU-NMS 根据中心距离惩罚重叠框多张戴了口罩的脸挤在一起时不会被一个高置信度大框整体吞掉。这些设计不像后来的 Anchor-Free 方法那样吃调参YOLOv4 把 anchor 数量、尺寸、mask 全都写进 cfg经验可以积累和迁移。相比 YOLOv5/v8YOLOv4 的另一个优势是 darknet 权重可以直接按 cfg 顺序装载到 PyTorch 模型里模型、权重、训练参数三个文件互相锚定出问题时可定位。这也是大量生产项目至今还在用“YOLOv4 训练 PyTorch 推理”组合的原因。后面第 4 章的权重导入就是这套组合的核心动作。2.2 PyTorch 安装与环境搭建GPU 驱动、CUDA 和 torch 版本怎么对齐环境搭建是第一个坑。很多项目包里的 requirements 写的是torch1.7但 YOLOv4 的 PyTorch 复现大多基于 1.x 编写直接用 PyTorch 2.x 也能跑问题往往出在 CUDA 版本与显卡不匹配导致torch.cuda.is_available()返回 False。常见的做法是用 conda 隔离环境按显卡选 CUDA 版本conda create -n mask python3.8 -y conda activate mask pip install torch1.12.1cu113 torchvision0.13.1cu113 --extra-index-url https://download.pytorch.org/whl/cu113第一条命令创建 Python 3.8 环境YOLOv4 的开源实现大多在 3.7~3.9 上验证3.8 是兼容面最宽的版本第二条命令安装 torch 1.12.1cu113 表示 CUDA 11.3。20 系、30 系显卡用 cu113 没问题40 系建议换成 cu118 的轮子。没有 NVIDIA 显卡也可以做 CPU 推理但速度大约只有 GPU 的 1/20训练基本不可行测试视频建议先用 GPU 机器验收。版本对齐建议按下面的表来确认避免装完再返工组件推荐配置原因Python3.8 或 3.10老复现代码在 3.9 有少数语法兼容问题PyTorch1.12 ~ 2.01.x 接口贴近 darknet 原生结构2.x 需额外编译适配CUDA11.3 / 11.830 系稳定40 系用 11.8 以上OpenCV4.5.5视频读取和 letterbox 预处理统一依赖它装好后先验证 PyTorch 张量基础torch.zeros(1, 3, 416, 416)能得到一个四维张量顺序是 NCHW即批次、通道、高、宽。推理时要把 OpenCV 读进来的 BGR 图先转成 RGB、归一化到 0~1、再 resize 到 416×416最后transpose成 CHW 并增加 batch 维度。这个预处理顺序错了模型输出的置信度会整体偏低但不会报错属于最难排查的一类问题。2.3 可复现工程里的目录骨架cfg、weights、docs 与测试视频的摆放一个能复现的 YOLOv4 口罩项目目录结构一般长这样mask_detect/ ├── cfg/ │ ├── yolov4-mask.cfg # 修改 classes 和 filters 后的网络结构 │ └── mask.names # 类别名两行with_mask、without_mask ├── weights/ │ ├── yolov4.weights # darknet 官方预训练权重 │ └── best.pt # 训练后的 PyTorch 权重 ├── data/ │ ├── images/ # 原始图片 │ ├── labels/ # 每张图对应的 YOLO 格式 txt │ ├── train.txt │ ├── valid.txt │ └── test_video.mp4 # 测试视频只用于验收不参与训练 ├── utils/ # 数据加载、增强、FPS 统计 ├── docs/ # 文档说明 ├── train.py ├── detect.py └── requirements.txt我一般会要求团队把测试视频单独放在 data 目录下而不是项目根目录因为它只承担验收职责放根目录容易被训练脚本误扫进数据集。docs 里的文档说明至少要写三件事数据格式定义、训练参数与官方默认值的差异、darknet 权重转换到 PyTorch 的方式。第 3 章和第 4 章的内容本质上就是这三份文档的展开。3. 人脸口罩数据准备与 YOLOv4 训练参数调整人脸口罩识别有两个数据上的固有难点一是“戴了口罩”这个类别有明确边界但口罩颜色接近肤色时模型容易学混二是许多数据集里with_mask远多于without_mask训练出来的模型惯性偏置把没戴口罩的人漏报。数据这一章要解决的是格式、增强和训练参数三者的一致性。3.1 labelme JSON 转 YOLO 格式标注坐标归一化YOLO 的标签格式是每个目标一行class_id cx cy w h其中 cx、cy 是中心点坐标w、h 是宽高四个值全部归一化到 0~1。现在标记得多的是 labelme 或者 X-AnyLabeling 输出的 JSON 文件需要转换为 txt。常见做法是按多边形外接矩形转换import json import os def convert_labelme(json_path, out_dir, class_mapNone): class_map class_map or {with_mask: 0, without_mask: 1} with open(json_path, r, encodingutf-8) as f: data json.load(f) h, w data[imageHeight], data[imageWidth] txt_name os.path.basename(json_path).replace(.json, .txt) lines [] for shape in data[shapes]: cls_id class_map.get(shape[label], 0) points shape[points] xs [p[0] for p in points] ys [p[1] for p in points] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) cx (x_min x_max) / 2 / w cy (y_min y_max) / 2 / h bw (x_max - x_min) / w bh (y_max - y_min) / h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(os.path.join(out_dir, txt_name), w) as f: f.write(\n.join(lines))这段代码的核心是标签归一化。归一化后训练时无论原图是 1920×1080 还是 720×1280标签都在 0~1 区间和网络的 grid 计算方式一致。类别编号必须和mask.names的行序一致也就是 cfg 里的 classes 数与其对应否则训练流程会跑完但 mAP 始终为零。另外注意外接矩形对人脸这种近似矩形的目标是够用的如果标注的是多边形而不是矩形直接取外接矩形不要让框过紧否则网络学到的回归目标方差太大。3.2 训练集划分与数据增强参数数据集量级上两个类别每类至少 3000 张其中 without_mask 建议单独多补 1000 张用来对抗类别不平衡。划分时用随机划分比例 8:2但要注意同一个人的多张照片不能既进训练集又进验证集否则验证集 mAP 虚高。增强策略按实际场景选择下表是我在口罩项目上的常用参数增强策略参数建议说明hue/sat/val0.01 / 0.5 / 0.5色调变化要小饱和度和亮度可以大一点mosaic4 张拼接显著改善半张脸小目标的召回水平翻转概率 0.5人脸左右对称翻转合理灰度化概率 0.05模拟单色摄像头画面防止过拟合颜色随机旋转不使用90 度旋转后人脸语义失效需要注意随机旋转 30 度以内虽然可以用但人脸检测框的宽高比会发生偏移在 YOLO 的 anchor 匹配中容易掉到不合适的分组所以不推荐。灰度化虽然只有 5% 概率但对红外闸机这类偏色严重的摄像头很有帮助代价几乎为零。3.3 改 cfg 与训练超参从 batch 到 filters 的一次改对YOLOv4 的 cfg 里每个检测头前面都有一个卷积层它的 filters 数量必须等于(5 num_classes) * 3。口罩识别只有 with_mask 和 without_mask 两类所以 filters 是(5 2) * 3 21。一共有三个 yolo 层每个 yolo 层前面的卷积层都要改成 21只改一个会导致权重加载时的 shape 不匹配。参数修改值含义width / height416 或 608输入分辨率低配显卡用 416classes2每个 yolo 层下的 classes 行filters21每个 yolo 层前一个卷积层batch64显存不够就调大 subdivisions 而不是调小 batchsubdivisions16相当于把 batch 切成 16 次前向learning_rate0.001微调权重复现常见burn_in1000前 1000 次迭代学习率线性升温max_batches6000自定义小数据集够用VOC 级数据要 50000steps4800, 5400分别对应 max_batches 的 80% 和 90%anchor 的修改要谨慎。官方 YOLOv4 预设了 9 组 anchor三个检测尺度的 mask 分配是52×52 用第 0、1、2 组对应小目标26×26 用第 3、4、5 组对应中目标13×13 用第 6、7、8 组对应大目标。口罩人脸框的宽高比通常在 0.7~1.3 之间官方 anchor 里的极端宽高比用不上但直接删改 anchor 会破坏 mask 映射容易造成训练不稳定。我一般保留官方的 9 组不动只调输入分辨率。3.4 训练产物、收敛判断与权重误用训练完成后不要只看 loss 降到多少那个数字和损失函数实现强相关跨项目对比没有意义。更实用的判断是看验证集 mAP0.5戴口类别单独统计without_mask 的 mAP 一般比 with_mask 低 3~5 个点如果低太多就要检查是不是漏检了。保存模型时建议同时保存 best.pt 和 last.ptbest 按 mAP0.5 的峰值保存last 用于断点续训。提示很多人拿到预训练权重后直接开始从头训练这会覆盖掉 COCO 的通用特征。口罩数据集不够大时正确做法是用 darknet 官方 yolov4.weights 作为初始权重并且确保加载时 filters 已经改成 21否则最后一层卷积的权重数量不同会在 shape 匹配检查时被跳过。4. PyTorch 实战加载 darknet 权重、解析三尺度输出并完成 NMS这个章节是标题里“源代码”的核心价值所在。YOLOv4 的训练在 darknet 上完成而部署推理放在 PyTorch 里做所以代码库里最关键的模块就是权重导入、前向解析和后处理。三者的顺序不能乱权重导错则输出全是噪声输出解析错则置信度恒等于 nan。4.1 在 PyTorch 里导入 darknet 权重头部跳过与按层装载darknet 的 .weights 是二进制顺序存储的没有层名只有按 cfg 顺序排列的浮点数。加载的第一步是跳过文件头。darknet 二进制文件头部有 3 个 int32 版本号加 1 个 int64 的训练计数共 20 字节有些编译器版本头只有 16 字节这也是不同转换脚本行为不一致的原因之一。常见做法是统一跳过 20 字节然后按卷积层顺序读取import numpy as np import torch def load_darknet_weights(model, weights_path): with open(weights_path, rb) as f: version f.read(20) # 跳过 darknet 头部 20 字节 flat np.fromfile(f, dtypenp.float32) ptr 0 for layer in model.layers: if layer.type ! convolutional: continue conv layer.conv if layer.bn is not None: n conv.weight.shape[0] bn layer.bn bn.bias.data.copy_(torch.from_numpy(flat[ptr:ptr n])); ptr n bn.weight.data.copy_(torch.from_numpy(flat[ptr:ptr n])); ptr n bn.running_mean.data.copy_(torch.from_numpy(flat[ptr:ptr n])); ptr n bn.running_var.data.copy_(torch.from_numpy(flat[ptr:ptr n])); ptr n shape conv.weight.data.shape # (out_c, in_c, k, k) numel int(np.prod(shape)) conv.weight.data.copy_(torch.from_numpy(flat[ptr:ptr numel]).view(shape)) ptr numel这个函数的逻辑是有 BatchNorm 的卷积层先加载 BN 的偏置、权重、均值、方差四个部分最后加载卷积核没有 BN 的层先加载卷积偏置再加载卷积核。因为 dense 块的组成顺序在 PyTorch 复现中严格对齐 darknet cfgptr 指针才能持续前进而不错位。如果加载后第一张测试图输出全是 0.5 左右通常就是头部跳过了 16 而不是 20 字节导致所有权重整体偏移了 4 个 float。4.2 三个尺度输出与检测头张量形状输入 416×416 时YOLOv4 输出三个张量分别对应 13×13、26×26、52×52 的特征图输出尺度张量形状对应目标分配 anchor大目标(1, 21, 13, 13)整张正脸(142,110) (192,243) (459,401)中目标(1, 21, 26, 26)戴帽、部分遮挡(36,75) (76,55) (72,146)小目标(1, 21, 52, 52)半张脸、远处人脸(12,16) (19,36) (40,28)张量形状里的第二个维度 21 是(5 2) * 3即 5 个坐标与置信度字段、2 个类别、3 个 anchor。网络输出是原始预测值需要解码成坐标解码过程是把张量 reshape 成(batch, 3, 5 num_classes, grid_h, grid_w)再调整维度顺序def decode_prediction(pred, anchors, num_classes): batch pred.shape[0] grid_h, grid_w pred.shape[2], pred.shape[3] pred pred.view(batch, len(anchors), 5 num_classes, grid_h, grid_w) pred pred.permute(0, 1, 3, 4, 2).contiguous() # (N, A, H, W, 5C) xy torch.sigmoid(pred[..., 0:2]) wh torch.exp(pred[..., 2:4]) * anchors.view(1, -1, 1, 1, 2).to(pred.device) conf torch.sigmoid(pred[..., 4:5]) cls torch.sigmoid(pred[..., 5:]) return xy, wh, conf, cls这里的参数含义需要说清楚xy 经过 sigmoid 映射到当前格子内部的 0~1 偏移wh 是指数回归再乘上对应 anchor 的宽高conf 代表目标置信cls 是每个类别的概率。很多源码里直接把 xy 加上 grid 偏移那是后处理的一部分和这个是同一步拆出来的。4.3 置信度过滤、NMS 与画框前的坐标回退拿到 conf 和 cls 之后先算最终分数score conf * max(cls)过滤掉低于阈值的框再做 NMS。PyTorch 直接用torchvision.ops.nms比手写快而且少出错from torchvision.ops import nms conf_thres 0.25 iou_thres 0.45 scores (conf * cls.max(dim-1).values)[0] mask scores conf_thres boxes xywh2xyxy(decoded_boxes)[0][mask] scores scores[mask] keep nms(boxes, scores, iou_thres)分数计算的顺序是 conf 先乘类别概率再做阈值过滤。如果直接把 conf 和 cls 分别过滤会出现“类别概率很高但目标置信很低”的框被误保留。参数上口罩类建议 conf_thres0.4without_mask 用 0.3后者漏检代价更高所以阈值放低一点。iou_thres 在密集人群里调到 0.4否则两个人脸框叠交叠严重时会把其中一个人丢掉。框坐标还有最后一个陷阱如果预处理用了 letterbox原图到网络输入之间有一段黑边补偿画框前要把网络的坐标按缩放比例和 pad 偏移回退到原图坐标而不是直接乘一个 resize 比例。这一步写错视频里检测框会整体向右下角偏移且越靠边缘偏移越明显。5. 用测试视频验收合批推理、FPS 统计与相邻帧投票项目包里的 test_video.mp4 是验收基准不是调参玩具。正确用法是把它当作固定测试集每次模型改动后跑一遍同一段视频记录检出率和 FPS再对比改动前后的差异。如果每次换不同视频调试无法判断模型是变好还是变坏。5.1 合批推理与真实 FPS 统计视频推理时逐帧调用 detect 函数是最直观的性能杀手。一次只推 1 帧GPU 利用率极低。常见做法是一次读 4 帧组成一个 batch送入网络后统一做后处理吞吐量能提高 2 倍以上显存增加很少。FPS 统计不要用 tick 打点除帧数那个值会被首帧预加载和 numpy 转张量的耗时拉低推荐用总帧数除以总耗时fps total_frames / (end_time - start_time)这样得到的是端到端真实吞吐量。5.2 相邻帧投票稳定检测框视频里检测框闪烁的根源不是模型抖动而是置信度在阈值附近震荡。给同一个目标加一个时间窗口投票能滤掉大部分临时误检class TemporalVote: def __init__(self, window5, min_votes3, iou_thres0.5): self.window window self.min_votes min_votes self.iou_thres iou_thres self.history [] def update(self, boxes, scores): self.history.append((boxes.clone(), scores.clone())) if len(self.history) self.window: self.history.pop(0) for i, box in enumerate(boxes): votes 0 for h_boxes, h_scores in self.history[-self.window:]: if len(h_boxes) 0: continue iou box_iou(box.unsqueeze(0), h_boxes) if (iou self.iou_thres).any(): votes 1 if votes self.min_votes: scores[i] 0.0投票机制的核心参数是 window 和 min_votes。window5 表示看最近 5 帧min_votes3 表示同一位置的框至少出现 3 次才保留。这样单帧的随机误检会在 3 帧内被沉默而真实目标因为连续出现在视频里不受影响。角注意如果视频剪辑有镜头切换切换后的第 1 帧不要立刻投票应先清空 history避免把切换前的目标带到新场景里。本文还有配套的精品资源点击获取
返回列表