基于Python深度学习的口罩佩戴检测与人脸识别系统实战
简介这份资源是一套基于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 的 transformsfrom torchvision import transforms train_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.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_classes2, freeze_backboneTrue): model models.resnet18(weightsmodels.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_backboneTrue时只训练新加的 fc 层训练快但精度上限低数据量够每类上千张时可以设 False 做全量微调。Dropout(0.3)是防过拟合的毕设数据集通常不大这个别省。注意weights参数在新版 torchvision 里替代了老的pretrainedTrue写错了会警告。3.2 训练循环里必须盯的三个数训练脚本本身不复杂但有几个数不盯就会白跑。下面是一个精简的训练循环import torch from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder def train(model, train_dir, val_dir, epochs15, lr1e-3, batch_size32): device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) train_ds ImageFolder(train_dir, transformtrain_tf) val_ds ImageFolder(val_dir, transformval_tf) train_loader DataLoader(train_ds, batch_sizebatch_size, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_sizebatch_size, shuffleFalse, num_workers4) criterion torch.nn.CrossEntropyLoss() # 只优化 requires_gradTrue 的参数 optimizer torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lrlr) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size5, gamma0.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(fEpoch {epoch1}: loss{total_loss/total:.4f}, ftrain_acc{train_acc:.4f}, val_acc{v_correct/v_total:.4f}) return model三个必须盯的数loss不降说明学习率太大或数据有问题train_acc和val_acc差距超过 10 个点就是过拟合得加数据或加 Dropoutval_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(weightweights.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_allTrue 表示一张图里检测多张脸 mtcnn MTCNN(keep_allTrue, devicecuda if torch.cuda.is_available() else cpu) # 预训练模型vggface2 数据集训练的输出 512 维特征 resnet InceptionResnetV1(pretrainedvggface2).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(dim1, keepdimTrue) b b / b.norm(dim1, keepdimTrue) return (a b.T).item()MTCNN会自动做人脸对齐把眼睛和鼻子摆正这一步对识别精度影响很大别跳过。InceptionResnetV1输出的 512 维向量就是人脸特征比对时算余弦相似度。阈值一般设 0.6 到 0.7低于这个值判为陌生人。这个阈值不是固定的得在你的验证集上试不同数据集最优阈值不一样。4.3 把两条链路串起来口罩检测和人脸识别要串成一个流程顺序很关键。正确顺序是先 MTCNN 检测人脸位置 → 裁剪出人脸区域 → 送入口罩分类模型判断 → 如果戴口罩再送人脸识别模型比对身份。为什么先检测再分类因为整张图直接送分类模型背景会干扰裁剪后精度明显更高。def process_frame(img_path, known_embeddings, threshold0.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, dtypenp.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(dim0) with open(gallery.pkl, wb) as f: pickle.dump(gallery, f) return gallery取平均是个小技巧比只用一张注册照鲁棒得多。如果某个人的照片质量参差不齐可以先算两两相似度把离群的那张剔掉再平均。这套流程跑下来注册 50 个人也就几分钟的事。最后说个我自己的教训别在训练集上反复调参调到 99% 就以为成了一定要留一份完全没碰过的测试集做最终验证。我见过太多毕设答辩时被老师现场换一张图就翻车的案例。把测试集当黑匣子跑完再打开看结果这个习惯能帮你省掉很多后悔药。希望帮到你。本文还有配套的精品资源点击获取