简介:这份资源是一套基于Python深度学习实现的口罩佩戴检测与人脸识别系统完整源码,面向计算机、电子信息等专业的学生与开发者,可作为课程设计、期末大作业或毕业设计的参考方案,帮助理解目标检测与人脸识别在实际项目中的落地方式。压缩包共86个文件,约120.16MB,包含21个py源码文件、19个pyc编译文件、17张jpg与jpeg图像、4个pth权重文件以及mp4演示视频、npy数据、ui界面文件等,覆盖模型推理、摄像头与视频检测、界面交互等模块。项目已提供requirements.txt依赖清单与main.py入口,按说明安装依赖后即可运行体验。目前已有437人学习下载,读者可从中获取完整的检测流程代码、预训练模型权重、人脸数据集与界面设计文件,便于快速复现效果并在此基础上进行二次开发与功能扩展。
1. 口罩佩戴检测加人脸识别,这套毕设方案到底解决什么问题
地铁口、医院大厅、工地入口,这类场景的管理诉求其实很朴素:谁进来了、有没有戴口罩。如果只做口罩检测,你只能知道"画面里有个人没戴",但不知道是谁;如果只做人脸识别,又没法判断这个人是否合规。把两者串起来,才是一个能落地的门禁级方案——先定位人脸,再判断口罩状态,最后对合规人脸做身份比对。
这套基于 Python 深度学习的口罩佩戴检测及人脸识别系统,核心就是两条流水线:一条做二分类(戴口罩/未戴口罩),一条做人脸特征提取与比对。它适合三类人:正在找高分毕设题目的同学、想快速搭一个 demo 验证想法的工程师、以及需要给现有门禁加一层口罩合规判断的开发者。源码结构通常包含数据集、训练脚本、推理脚本和一份项目说明,拿到手能直接跑通是最低要求。下面我按"能复现"的标准,把整条链路拆开讲清楚。
2. 环境搭建与数据集准备:从 python 安装到能跑通第一张图
2.1 为什么选 PyTorch 而不是 TensorFlow
做这个题目,框架选型直接决定你后面踩多少坑。我一般推荐 PyTorch,原因很实际:口罩检测这类小数据集任务,PyTorch 的动态图调试体验好太多,出错了能直接 print 中间张量;而且 torchvision 里现成的 ResNet、MobileNet 拿来改分类头就能用,迁移学习成本极低。TensorFlow 不是不行,但 2.x 之后 API 变动大,网上搜到的老教程经常对不上,对新手不友好。
人脸识别这条线,常见做法是用 facenet-pytorch 或者 insightface。前者安装简单、预训练权重直接下载,适合毕设;后者精度更高但依赖编译环境,容易在 Windows 上翻车。如果你只是想跑通,先用 facenet-pytorch 的 MTCNN 做人脸检测 + InceptionResnetV1 做特征提取,够用了。
环境版本上,Python 建议 3.8 到 3.10,太新的 3.12 有些包还没适配。CUDA 版本跟着你的显卡驱动走,没有 N 卡就用 CPU 跑,只是训练慢。下面是我常用的安装命令:
# 创建虚拟环境,避免污染全局 python -m venv mask_env # Windows 激活 mask_env\Scripts\activate # Linux/Mac 激活 source mask_env/bin/activate # 安装核心依赖,torch 版本按你的 CUDA 选 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python numpy pillow matplotlib pip install facenet-pytorch这里--index-url指定的是 PyTorch 官方源,cu118 对应 CUDA 11.8,你要根据自己的驱动改。装完用python -c "import torch; print(torch.cuda.is_available())"验证,返回 True 说明 GPU 可用。这一步是很多人的第一个翻车点——装完发现是 CPU 版,训练慢十倍。
2.2 数据集怎么组织才不用改代码
口罩检测的公开数据集不少,常见的是 Kaggle 上的 Mask Detection 和 RMFD。但下载下来目录结构五花八门,直接喂给 ImageFolder 经常报错。我一般会统一成这样的结构:
dataset/ ├── train/ │ ├── with_mask/ │ └── without_mask/ ├── val/ │ ├── with_mask/ │ └── without_mask/with_mask和without_mask各放对应图片,ImageFolder 会自动按文件夹名生成标签。人脸识别部分需要的是"每个人一个文件夹"的结构:
faces/ ├── person_01/ │ ├── img1.jpg │ └── img2.jpg ├── person_02/ │ └── ...这里有个血泪经验:口罩数据集里经常混入卡通图、灰度图、甚至损坏文件,训练时突然报PIL.UnidentifiedImageError。写个清洗脚本先过一遍:
import os from PIL import Image def clean_dataset(root): bad = [] for dirpath, _, filenames in os.walk(root): for fn in filenames: fp = os.path.join(dirpath, fn) try: img = Image.open(fp) img.verify() # 校验文件完整性 except Exception as e: bad.append(fp) os.remove(fp) # 损坏的直接删 print(f"清理了 {len(bad)} 个坏文件") return bad clean_dataset("dataset/train")img.verify()只检查文件头,不加载像素,速度快。注意 verify 之后如果还要用这个 img 对象得重新 open,因为 verify 会让它失效。这个脚本跑一遍,能省掉后面训练到一半崩掉的麻烦。
2.3 数据增强的度怎么把握
口罩检测有个特殊性:口罩本身遮挡了下半张脸,模型很容易学到"白色矩形=戴口罩"这种捷径。所以增强策略要针对性地破坏这种捷径。我一般用 torchvision 的 transforms:
from torchvision import transforms train_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(15), # 小角度旋转,模拟头部倾斜 transforms.ColorJitter(0.2, 0.2, 0.2), # 颜色抖动,防止只认白色 transforms.RandomAffine(0, translate=(0.1, 0.1)), # 平移 transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ])Normalize里的均值方差是 ImageNet 的统计值,用预训练模型就必须这么归一化,否则精度掉得莫名其妙。ColorJitter是关键,它让模型不能只靠颜色判断口罩,得看形状和位置。旋转角度别超过 20 度,太大反而不像真实场景。
3. 口罩检测模型训练:迁移学习怎么改才不浪费预训练权重
3.1 用 ResNet18 改分类头的最小改动
从零训练一个 CNN 在小数据集上基本没戏,迁移学习是标配。ResNet18 参数量小、推理快,适合毕设这种要演示实时性的场景。改分类头的代码:
import torch.nn as nn from torchvision import models def build_model(num_classes=2, freeze_backbone=True): model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) if freeze_backbone: # 冻结前面的卷积层,只训练最后的全连接 for param in model.parameters(): param.requires_grad = False # 替换最后的 fc 层,输入维度 512 是 ResNet18 的特征维度 in_features = model.fc.in_features model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) return modelfreeze_backbone=True时只训练新加的 fc 层,训练快但精度上限低;数据量够(每类上千张)时可以设 False 做全量微调。Dropout(0.3)是防过拟合的,毕设数据集通常不大,这个别省。注意weights参数在新版 torchvision 里替代了老的pretrained=True,写错了会警告。
3.2 训练循环里必须盯的三个数
训练脚本本身不复杂,但有几个数不盯就会白跑。下面是一个精简的训练循环:
import torch from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder def train(model, train_dir, val_dir, epochs=15, lr=1e-3, batch_size=32): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) train_ds = ImageFolder(train_dir, transform=train_tf) val_ds = ImageFolder(val_dir, transform=val_tf) train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=4) criterion = torch.nn.CrossEntropyLoss() # 只优化 requires_grad=True 的参数 optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=lr) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5) for epoch in range(epochs): model.train() total_loss, correct, total = 0, 0, 0 for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() out = model(imgs) loss = criterion(out, labels) loss.backward() optimizer.step() total_loss += loss.item() * imgs.size(0) correct += (out.argmax(1) == labels).sum().item() total += imgs.size(0) scheduler.step() train_acc = correct / total # 验证 model.eval() v_correct, v_total = 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) out = model(imgs) v_correct += (out.argmax(1) == labels).sum().item() v_total += imgs.size(0) print(f"Epoch {epoch+1}: loss={total_loss/total:.4f}, " f"train_acc={train_acc:.4f}, val_acc={v_correct/v_total:.4f}") return model三个必须盯的数:loss不降说明学习率太大或数据有问题;train_acc和val_acc差距超过 10 个点就是过拟合,得加数据或加 Dropout;val_acc震荡说明 batch_size 太小或学习率没衰减。StepLR每 5 个 epoch 把学习率砍半,是让后期收敛更稳的常用手段。
3.3 类别不平衡时怎么调
真实场景里"戴口罩"的样本远多于"未戴口罩",直接训练模型会偏向多数类,导致漏检。两个办法:一是给损失函数加权重,二是重采样。加权重更简单:
# 假设 with_mask 有 5000 张,without_mask 有 1000 张 # 权重和样本数成反比 class_counts = [5000, 1000] weights = torch.tensor([1.0 / c for c in class_counts]) weights = weights / weights.sum() # 归一化 criterion = torch.nn.CrossEntropyLoss(weight=weights.to(device))这样少数类的 loss 贡献被放大,模型不敢忽略。注意权重别设得太极端,否则模型会对少数类过拟合,验证集上反而掉点。一般控制在 5:1 以内比较稳。
4. 人脸识别接入:MTCNN 检测加特征比对的完整链路
4.1 为什么用 MTCNN 而不是 Haar 级联
人脸检测这一步,OpenCV 自带的 Haar 级联速度快但误检多,侧脸、遮挡基本失效。MTCNN 是三级级联网络,对侧脸和小脸更鲁棒,而且 facenet-pytorch 里直接封装好了,一行代码调用。代价是慢一些,但毕设演示够用。如果要做实时视频,可以先用 Haar 粗筛再用 MTCNN 精检,或者直接上 RetinaFace。
4.2 人脸特征提取与比对代码
import torch from facenet_pytorch import MTCNN, InceptionResnetV1 from PIL import Image # 初始化,keep_all=True 表示一张图里检测多张脸 mtcnn = MTCNN(keep_all=True, device='cuda' if torch.cuda.is_available() else 'cpu') # 预训练模型,vggface2 数据集训练的,输出 512 维特征 resnet = InceptionResnetV1(pretrained='vggface2').eval().to(mtcnn.device) def get_embedding(img_path): img = Image.open(img_path).convert('RGB') # 检测并对齐人脸,返回的是归一化后的张量 faces = mtcnn(img) if faces is None: return None with torch.no_grad(): # faces 形状 [n, 3, 160, 160],输出 [n, 512] emb = resnet(faces.to(mtcnn.device)) return emb def cosine_sim(a, b): # 余弦相似度,值越接近 1 越像 a = a / a.norm(dim=1, keepdim=True) b = b / b.norm(dim=1, keepdim=True) return (a @ b.T).item()MTCNN会自动做人脸对齐(把眼睛和鼻子摆正),这一步对识别精度影响很大,别跳过。InceptionResnetV1输出的 512 维向量就是人脸特征,比对时算余弦相似度。阈值一般设 0.6 到 0.7,低于这个值判为陌生人。这个阈值不是固定的,得在你的验证集上试,不同数据集最优阈值不一样。
4.3 把两条链路串起来
口罩检测和人脸识别要串成一个流程,顺序很关键。正确顺序是:先 MTCNN 检测人脸位置 → 裁剪出人脸区域 → 送入口罩分类模型判断 → 如果戴口罩,再送人脸识别模型比对身份。为什么先检测再分类?因为整张图直接送分类模型,背景会干扰,裁剪后精度明显更高。
def process_frame(img_path, known_embeddings, threshold=0.65): img = Image.open(img_path).convert('RGB') boxes, _ = mtcnn.detect(img) # 只检测不裁剪 if boxes is None: return [] results = [] for box in boxes: x1, y1, x2, y2 = [int(b) for b in box] face_crop = img.crop((x1, y1, x2, y2)) # 口罩分类 face_tensor = val_tf(face_crop).unsqueeze(0).to(device) with torch.no_grad(): mask_out = mask_model(face_tensor) mask_pred = mask_out.argmax(1).item() # 只有戴口罩才做人脸识别 if mask_pred == 0: # 假设 0 是 with_mask emb = get_embedding_from_crop(face_crop) best_sim, best_id = 0, "unknown" for pid, known_emb in known_embeddings.items(): sim = cosine_sim(emb, known_emb) if sim > best_sim: best_sim, best_id = sim, pid if best_sim < threshold: best_id = "unknown" results.append((box, "with_mask", best_id, best_sim)) else: results.append((box, "without_mask", None, 0)) return results这段代码把两条链路合在一起,返回每个人脸的位置、口罩状态、身份和相似度。实际部署时known_embeddings是提前对每个注册人员算好存起来的,不用每次重算,这是性能优化的关键。
5. 避坑与排查:这套系统最容易翻车的五个地方
5.1 训练 loss 正常但验证集精度不动
现象:训练集准确率冲到 95%,验证集卡在 60% 上不去。原因通常是数据泄漏——训练集和验证集里有同一批人的照片,模型记住了人脸而不是口罩特征。解决:按人划分数据集,同一个人不能同时出现在训练和验证集里。这个坑在毕设里特别常见,因为很多人直接随机 split。
5.2 摄像头实时推理卡成幻灯片
现象:单张图推理 50ms,但接摄像头后帧率只有 3 帧。原因是每帧都跑了 MTCNN + 口罩分类 + 人脸识别三个模型,而且没做批处理。解决:降低检测频率,比如每 5 帧检测一次人脸,中间帧复用上一次的框;或者把 MTCNN 换成更轻的检测器。另外torch.no_grad()一定要加,否则会存计算图,显存暴涨。
5.3 戴口罩的人脸识别精度暴跌
现象:同一个人,不戴口罩识别准确率 98%,戴上口罩掉到 70%。原因是人脸识别模型是在完整人脸上训练的,口罩遮挡了下半脸,特征分布变了。解决:一是只用上半脸特征做比对,二是注册时也存一张戴口罩的照片,三是降低相似度阈值。实际项目里我一般会同时存戴口罩和不戴口罩的注册照,比对时取最高分。
5.4 中文路径导致读取失败
现象:cv2.imread返回 None,或者 PIL 报错。原因是 OpenCV 的 imread 不支持中文路径。解决:用cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR)替代,或者干脆统一用 PIL 读图再转 numpy。这个坑在 Windows 中文系统上几乎必踩。
5.5 模型保存后加载报 key 不匹配
现象:训练完保存torch.save(model.state_dict(), 'model.pth'),加载时model.load_state_dict()报 missing keys。原因是保存时模型结构和加载时不一致,比如保存时改了 fc 层,加载时用的是原始 ResNet。解决:加载前先用同样的build_model()构建结构,再 load。或者保存整个模型torch.save(model, ...),但这样依赖类定义,不推荐。
6. 从能跑到好用:阈值调优和批量注册的实战技巧
跑通只是起点,真正决定这套系统好不好用的是两个细节:相似度阈值怎么定,以及注册库怎么维护。
阈值不是拍脑袋定的。我的做法是准备一组标注好的测试对——同一个人的两张不同照片算正样本对,不同人的算负样本对,各准备 50 对以上。然后遍历阈值从 0.4 到 0.9,算准确率和召回率:
import numpy as np def find_best_threshold(pos_sims, neg_sims): # pos_sims: 正样本对的相似度列表 # neg_sims: 负样本对的相似度列表 best_thr, best_acc = 0.5, 0 for thr in np.arange(0.4, 0.91, 0.01): # 正样本判对:相似度 >= 阈值 tp = sum(1 for s in pos_sims if s >= thr) # 负样本判对:相似度 < 阈值 tn = sum(1 for s in neg_sims if s < thr) acc = (tp + tn) / (len(pos_sims) + len(neg_sims)) if acc > best_acc: best_acc, best_thr = acc, thr return best_thr, best_acc跑完你会得到一条准确率曲线,取峰值对应的阈值。注意这个阈值和你的注册库质量强相关——注册照越清晰、角度越正,阈值可以设得越高。我一般会把最终阈值定在峰值略低一点的位置,宁可多认几个熟人,也别把熟人挡在门外。
批量注册这块,很多人是一张一张手动加,人一多就乱。我的习惯是写个脚本扫描注册目录,自动算特征存成字典:
import os, pickle def build_gallery(faces_root): gallery = {} for person in os.listdir(faces_root): person_dir = os.path.join(faces_root, person) if not os.path.isdir(person_dir): continue embs = [] for fn in os.listdir(person_dir): emb = get_embedding(os.path.join(person_dir, fn)) if emb is not None: embs.append(emb) if embs: # 同一个人的多张照片取平均,比单张更稳 gallery[person] = torch.stack(embs).mean(dim=0) with open("gallery.pkl", "wb") as f: pickle.dump(gallery, f) return gallery取平均是个小技巧,比只用一张注册照鲁棒得多。如果某个人的照片质量参差不齐,可以先算两两相似度,把离群的那张剔掉再平均。这套流程跑下来,注册 50 个人也就几分钟的事。
最后说个我自己的教训:别在训练集上反复调参调到 99% 就以为成了,一定要留一份完全没碰过的测试集做最终验证。我见过太多毕设答辩时被老师现场换一张图就翻车的案例。把测试集当黑匣子,跑完再打开看结果,这个习惯能帮你省掉很多后悔药。希望帮到你。
本文还有配套的精品资源,点击获取