简介:面向计算机视觉与人脸识别方向的课程设计与毕业设计需求,这份口罩人脸数据集提供了可直接用于模型训练与效果验证的图像资源。压缩包内共1222个文件,以1200张jpg格式人脸图像为绝对主体,可用于口罩佩戴检测等任务的训练与测试;同时包含7个rar压缩包、5个gif演示动画、5个png示例图、3个md说明文档以及1个Python脚本,分别承担数据补充、效果展示、项目说明和辅助脚本等功能,方便使用者快速上手。整体大小约178.32MB,目录结构清晰,便于直接导入PyTorch、TensorFlow等常见深度学习框架开展实验。目前已有105人学习下载,适合具备一定Python与深度学习基础的学生进行实战练习,也可作为课程大作业、课程设计或毕业设计的初期项目演示。借助整理好的图像数据与配套脚本,使用者能大幅减少数据收集与预处理时间,更专注于模型调优、效果验证与最终项目输出。
1. 口罩人脸数据集不是用来“刷量”的,先解决读图问题再谈训练
口罩人脸数据集.zip 拿回来后,第一件事不是找训练脚本,而是想清楚:examples 目录里 GIF 和 JPG 混在一起,每张图的脸部位置、口罩覆盖范围都不一样,命名还带着明显的采集者标识。直接把路径喂给现成的 YOLO 脚本,大概率会卡在“文件读不出来”或者“标签对不上”这两类错误上。
这个资源的价值不在规模大,而在于它是真实采集的口罩遮挡样本,能把“模型在公开数据集上能跑、在这个数据上识别不准”的问题暴露出来。它适合做课程大作业、课程设计、毕业设计的种子数据,也适合拿来给初期立项做演示。下面从数据解析、标注、扩增、训练到评估,一步一步拆开讲。
2. GIF/JPG 混合样本的解析与清洗,先从 examples 目录验证读图能力
下载资源里最常见的一个陷阱是:打开压缩包看到一堆文件,就直接把路径传给训练脚本,等报错再回头查。这个数据集里既有动图又有静图,最先要解决的不是算法,而是“哪些文件能直接读、哪些必须抽帧、哪些文件名里藏着坑”。
2.1 文件命名和格式能告诉我们哪些信息
先看 examples 目录里实际出现的文件:wnx_.gif、12.gif、wuhao.gif、wnx.gif、hzb.gif、Wang.jpg,以及三个十六进制命名的 JPG。wnx_.gif 和 wnx.gif 属于同一命名前缀的两份动图,带下划线的那个通常是第二次保存时自动追加的;12.gif 这类纯数字命名,一般是批量抓取脚本按序号生成的;Wang.jpg 是姓氏命名的手工采集单帧;三个十六进制文件名(29C1E67A...、23872B63...、0CAD80FF...)大概率来自摄像头 SDK 或手机自动存档。
从这些名字本身只能推断来源,看不出戴口罩与否,也没有任何标签文件,所以标注这一步绕不开。格式差异带来的处理负担也不一样,整理成表如下:
| 文件名特征 | 后缀 | 推断来源 | 是否要特殊处理 |
|---|---|---|---|
| wnx_.gif / wnx.gif | GIF | 同人动图重复保存 | 需要抽帧,注意内容重复 |
| 12.gif | GIF | 脚本批量抓取 | 需要抽帧 |
| wuhao.gif / hzb.gif | GIF | 以拼音/缩写命名 | 需要抽帧 |
| Wang.jpg | JPG | 手工采集单帧 | 直接可读 |
| 十六进制 JPG | JPG | 摄像头/SDK 存档 | 直接可读,但需确认朝向 |
这里有个隐蔽问题:OpenCV 的 cv2.imread 读 GIF 时默认只取第一帧。文件不报错,但你的训练集会悄悄少掉大量数据。我一般先统计每个 GIF 的帧数再决定抽帧策略,而不是直接用 cv2.VideoCapture 去读,因为后者对调色板索引型 GIF 兼容性不稳定。
2.2 用 PIL 抽帧,保留动图的完整信息
常见做法是用 Pillow 的 ImageSequence 把 GIF 逐帧拆开,转成 RGB 后落盘。代码如下:
from pathlib import Path from PIL import Image, ImageSequence def gif_to_frames(gif_path, out_dir, step=1): img = Image.open(gif_path) print("格式:", img.format, "索引模式:", img.mode) frames = [] for i, frame in enumerate(ImageSequence.Iterator(img)): # step 控制抽帧密度,动作连续的 GIF 不必每帧都留 if i % step != 0: continue # 调色板模式(P)和灰度模式(L)统一转 RGB,避免通道数不一致 f = frame.convert("RGB") if frame.mode in ("P", "L") else frame frames.append(f.copy()) out = Path(out_dir) out.mkdir(parents=True, exist_ok=True) stem = Path(gif_path).stem for i, f in enumerate(frames): f.save(out / f"{stem}_f{i:03d}.jpg", quality=95) return len(frames) if __name__ == "__main__": print(gif_to_frames("examples/wnx.gif", "data/raw/wnx", step=2))这段逻辑是:ImageSequence.Iterator 逐个吐出 GIF 的帧对象,取出后先判断图像模式。很多表情包类 GIF 存的是 8 位调色板索引图,不转 RGB 直接保存,后续用 PyTorch 或 TensorFlow 读图时容易出现通道数报错。step 参数控制每多少帧保留一帧,比如 step=2 就是隔一帧取一帧。
参数说明:gif_path 是输入的 GIF 路径;out_dir 是输出目录,按文件名自动建子目录;step 建议先看总帧数再定,帧数超过 30 的动图用 step=2 或 3,既能保留动作变化,又减少相邻帧高度相似带来的冗余。保存时用 quality=95,比默认 75 更接近原图质量,对之后微调模型有帮助。
2.3 解压报错先验证 zip 完整性,别急着换工具
数据集是通过 zip 分发的,下载中断或者网盘中转丢包都会导致解压失败。最常见报错是 End-of-central-directory signature not found,有的框架会包装成 could not find EOCD,还有的会在导入资源包时提示 invalid zip archive。这些基本都是同一个原因:文件没下全。
我一般先做一次完整性检查:
unzip -t 口罩人脸数据集.zip-t 参数是测试压缩包完整性,不实际解压。输出 No errors detected in compressed data of 口罩人脸数据集.zip 表示文件完好;如果输出某个文件名和 CRC 错误,说明该文件在传输过程中坏了,重新下载比尝试修复更省时间。7-Zip 打开时如果只显示部分文件,也说明同样的传输问题。
提示:GIF 在系统缩略图里只渲染第一帧,抽帧完成后要抽查中间帧,动作幅度大的动图拆出来的帧信息量更足,对训练更有价值。
3. 标签组织与图像扩增,把几十个原始样本变成项目资源
解析完格式后,下一个问题是监督信息从哪来。这个压缩包里没有现成的 XML 或 JSON 标签,所以得自己构建“戴口罩/未戴口罩”两类标签。小样本场景下,用目录结构代替标注文件是最快、最不容易错的做法。
3.1 目录即标签,先搭出训练集骨架
先建好 train 和 val 两个大目录,下面再分 mask 和 nomask 两个类别目录。这样 PyTorch 的 ImageFolder 和 TensorFlow 的 image_dataset_from_directory 都能直接消费,不需要额外写解析器:
mkdir -p data/train/mask data/train/nomask mkdir -p data/val/mask data/val/nomask把抽帧得到的图片按内容放进对应目录。判断依据只能靠人工看图:口罩覆盖口鼻的是 mask,没有遮挡的是 nomask。部分 GIF 帧里人脸太小或严重模糊,直接丢弃,不要硬留。这个筛选动作很重要,模糊帧会拉低整个模型的收敛速度。
如果你需要的是目标检测(把每张脸框出来),那就得用 LabelImg 或 X-AnyLabeling 画边界框,工作量会大一个量级。对于课程设计和大作业,我建议先用分类方案把识别流程跑通,再决定要不要升级成检测。分类任务能演示的核心能力,包括“输入图片、输出是否佩戴口罩”,已经足够覆盖大部分毕设的验收要求。
3.2 扩增参数怎么定,直接照抄这张表容易翻车吗
扩增是缓解小数据集过拟合最直接的手段,但参数设得太激进会让样本失真。口罩人脸的特殊性在于:口罩有明显的上下语义,翻转、旋转要有限度。我把常用操作和参数范围整理如下:
| 扩增操作 | 参数范围 | 注意事项 |
|---|---|---|
| 水平翻转 | 概率 0.5 | 口罩左右对称,水平翻转安全 |
| 垂直翻转 | 禁止 | 墨镜/口罩上下颠倒,语义错误 |
| 随机旋转 | ±12° | 超过 15° 会切掉面部主体 |
| 亮度扰动 | 0.8 ~ 1.2 | 模拟室内外光照差异 |
| 高斯噪声 | sigma 0.01 | 模拟摄像头低光噪声 |
| 随机裁剪 | 0.85 ~ 1.0 | 裁剪后要 resize 回原尺寸 |
| HSV 色相偏移 | h ±10 | 避免肤色/dot 类口罩颜色偏移过大 |
3.3 批量扩增代码与落盘策略
扩增代码用 OpenCV 写,训练前一次性生成静态文件,比在 Dataset 里做在线扩增更容易排查问题:
import cv2 import numpy as np from pathlib import Path def augment_images(src_dir, dst_dir, copies=3): src_dir = Path(src_dir) dst_dir = Path(dst_dir) dst_dir.mkdir(parents=True, exist_ok=True) for p in src_dir.glob("*.jpg"): img = cv2.imread(str(p)) h, w = img.shape[:2] # 水平翻转 cv2.imwrite(str(dst_dir / f"{p.stem}_hf.jpg"), cv2.flip(img, 1)) # 小角度旋转,背景填充用边缘复制,避免黑边 for k, angle in enumerate([-10, 10]): M = cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0) rot = cv2.warpAffine(img, M, (w, h), borderMode=cv2.BORDER_REPLICATE) cv2.imwrite(str(dst_dir / f"{p.stem}_rot{k}.jpg"), rot) # 亮度缩放 hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) hsv[:, :, 2] = cv2.multiply(hsv[:, :, 2], 1.15) bright = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR) cv2.imwrite(str(dst_dir / f"{p.stem}_br.jpg"), bright)这里 cv2.flip(img, 1) 的第二个参数 1 表示水平翻转,0 是垂直翻转,我这里故意没写垂直翻转分支,因为在口罩场景里不能做垂直翻转。cv2.getRotationMatrix2D 以图片中心旋转,角度为 -10 和 10,borderMode 用 BORDER_REPLICATE 复制边缘像素,避免旋转后出现黑色填充区域,这些黑边会影响模型对肤色和口罩边缘的学习。
3.4 扩增后的切分要按“来源”分,不能按“帧”分
这是最容易踩的坑。wnx.gif 抽出来 20 帧,如果把其中 16 帧放 train、4 帧放 val,模型在验证集上的准确率会虚高,因为它见过同一动图的其他帧,基本等于“开卷考试”。正确做法是把一个 GIF 的所有帧全部放进同一个集合。
实践中的操作方法是:先按文件来源分组,把整组随机分配到 train 或 val,再在组内做扩增。扩增样本也跟随原图所在集合,不跨集复制。这样验证集的评估结果才真实反映模型对“没见过的人/场景”的泛化能力。
4. 轻量级 CNN 微调,小数据场景的参数设计比模型选择更重要
数据集只有几十个原始样本、几百张扩增图,这种情况下训一个大模型没有任何意义。常见做法是选一个轻量级预训练网络做微调,把精力放在数据加载、冻结策略和学习率这些参数上。
4.1 backbone 选 MobileNetV3 而不是 ResNet50
口罩检测任务输入是 224x224 的人脸图,判断目标是“是否佩戴口罩”。ResNet50 在这个尺度上参数量的优势发挥不出来,反而容易在几千张图上快速过拟合。我一般用 torchvision 里的 mobilenet_v3_large,输入 224x224,全连接层改成二分类输出。
改最后一层时注意保持前层预训练权重不变:
import torchvision.models as models model = models.mobilenet_v3_large(weights=models.MobileNet_V3_Large_Weights.IMAGENET1K_V1) num_ftrs = model.classifier[-1].in_features model.classifier[-1] = torch.nn.Linear(num_ftrs, 2)逻辑:取出 MobileNetV3 分类头的最后一层输入维度 in_features,替换成输出 2 个类别的线性层。必须用 in_features 动态获取,不能写死 1280,因为 torchvision 不同版本这个数字可能变化。
参数说明:weights=IMAGENET1K_V1 加载 ImageNet 预训练权重,两层分类头后面的线性层默认参数是随机的,需要训练,而前面的卷积特征提取层则保持预训练状态,先不更新,后面微调时再决定解冻哪些层。
4.2 Dataset 与 DataLoader 参数表
PyTorch 用 ImageFolder 就能直接加载前面建好的目录结构:
from torchvision.transforms import v2 from torch.utils.data import DataLoader tf = v2.Compose([ v2.Resize((224, 224)), v2.ToImage(), v2.ToDtype(torch.float32, scale=True), v2.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_ds = torchvision.datasets.ImageFolder("data/train", transform=tf) val_ds = torchvision.datasets.ImageFolder("data/val", transform=tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=2, pin_memory=True) val_loader = DataLoader(val_ds, batch_size=64, shuffle=False, num_workers=2)这里有几个参数需要重点解释,写论文或答辩时被问到的概率很高:
| DataLoader 参数 | 本项目建议值 | 说明 |
|---|---|---|
| batch_size | 32 | 小于 16 时 BatchNorm 统计量不稳定 |
| shuffle | 训练集 True | 打乱顺序避免模型按序学习 |
| num_workers | 2~4 | CPU 核数一半即可,太大会拖慢 |
| pin_memory | True | 加快 GPU 拷贝,无 GPU 时可设 False |
| drop_last | 视情况 | 样本数不能被 batch 整除时丢弃末尾 |
Normalize 使用的 mean 和 std 是 ImageNet 统计值,迁移学习时保持和预训练一致,不需要自己重新统计均值,否则前面加载的权重白加载了。
4.3 冻结特征层,只训分类头
冻结策略上,我先固定整个 backbone,只训练最后一层分类头。这一步收敛很快,通常十几个 epoch 就能看到验证集准确率有区分度。然后再解冻最后几个 stage,用更低的学习率微调特征层。
for name, param in model.named_parameters(): if "classifier" not in name: param.requires_grad = False optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3) loss_fn = torch.nn.CrossEntropyLoss()逻辑说明:将 classifier 之外的层全部固定,相当于只让最后一层在随机初始化基础上学习,避免初期梯度把预训练特征带偏。不要一开始就解冻全部层并设 lr=1e-3,那样的更新幅度是由 ImageNet 学到的特征在发酵反转,十几张图的迭代几乎损失在塑料位置上。
参数说明:分类头阶段用 lr=1e-3,不需要权重衰减,类别只有两类,分类头的波动是良性的。等分类头收敛,再手动把“最后两个 block”这类的 param.requires_grad 改成 True,优化器换成 lr=1e-5,做整网微调。
训练循环这边提一个关键点:每个 epoch 结束都要在 val_loader 上算准确率,保存最优模型而不是最后 epoch 的模型。小数据集过拟合发生得很快,可能第 15 个 epoch 是峰值,第 18 个 epoch 已经在退化了。
5. 用 ROC 选阈值而不是硬等 0.5,口罩识别的验收技巧
很多课程设计和毕设的代码里,最后一行的判断都是 score > 0.5。这个默认逻辑在小样本迁移学习里其实并不稳妥,因为两类样本数量很难做到严格均衡,而且 GIF 抽帧出来的低画质帧会让模型输出的概率分布整体偏移。
5.1 先看验证集上的正负样本比例
你前面手动分类的时候,mask 和 nomask 不一定各占一半。实际采集场景里,戴口罩的 GIF 数量往往比不带口罩的多。如果验证集里 70% 是 mask,那么模型全输出 mask 也有 70% 准确率,光看准确率根本发现不了它已经退化成“猜多数类”。
我的做法是计算一个最简单的类别占比:
from collections import Counter label_counts = Counter(label for _, label in val_ds) total = len(val_ds) print({c: n / total for c, n in label_counts.items()})当占比超过 0.65 / 0.35 时,表层准确率就不如 F1 和 ROC-AUC 更能反映模型真实能力。把这个结果直接放进课程设计报告的数据分析章节,算是从“跑通”到“分析清楚了”的一个分水岭。
5.2 按最大约登指数找判决阈值
模型输出的是 softmax 概率,默认阈值 0.5 只是“网络认为两边概率相等”的语义点,并没有考虑训练集先验。更稳的做法是在验证集上画出 ROC 曲线,选取约登指数(Youden's J)最大的点作为阈值:
import numpy as np from sklearn.metrics import roc_curve, auc y_true = [] y_score = [] for imgs, labels in val_loader: outs = torch.softmax(model(imgs), dim=1)[:, 1] y_true.extend(labels.numpy()) y_score.extend(outs.detach().numpy()) fpr, tpr, thresholds = roc_curve(y_true, y_score) j = tpr - fpr best_idx = int(np.argmax(j)) print("AUC:", auc(fpr, tpr)) print("best threshold:", thresholds[best_idx], "约登指数:", j[best_idx])逻辑说明:roc_curve 会按所有可能阈值计算假正率 fpr 和真正率 tpr,阈值的候选点就是模型在验证集上输出的每个概率值。约登指数 = tpr - fpr,最大值对应“漏检损失 + 误检损失”综合最小的点。测试时用这个自定义阈值代替固定的 0.5。
5.3 最后一轮验收的三个动作
第一个动作,数一遍 gif 抽帧后的图片数量,和帧数对上。结课答辩时老师如果问“哪些文件是抽帧得到的”,你答不上文件数就露馅。第二个动作,把同一 GIF 早期帧与后期帧的预测结果打印出来,确认扩增没有把帧序切散到 train 和 val 两侧。第三个动作是盲测:留出几张原始 JPG 和几张抽帧前完全没参与扩增的帧,单独跑一遍推理,观察阈值调整前后哪几张被纠正。
小数据集中,阈值偏移造成的误判往往比模型架构问题更常见。一张曝光偏暗的戴口罩侧脸,softmax 输出可能是 0.47 vs 0.53,如果你把阈值按上面代码校准到 0.42,它就能被正确识别。把这种细节作为项目源码的注释写在 predict 函数里,比堆模型参数量更能体现你把数据链路跑通了。
本文还有配套的精品资源,点击获取