简介基于Faster RCNN的人脸口罩识别系统是一套面向计算机、人工智能等专业的课程设计与毕设项目源码包解决人脸检测与口罩佩戴识别从模型训练到Streamlit部署的完整流程问题。训练脚本负责读取按姓名归类的图片并生成人脸特征模型识别脚本则借助Streamlit提供交互式口罩人脸识别界面便于快速验证效果。包内共5个文件包含两个Python脚本分别承担数据集训练与识别部署两个示意图辅助说明界面效果一个Markdown文档提供运行说明与目录结构压缩包仅881KB轻量易用。代码预留了按“dataset/姓名”组织训练图片的目录结构并在说明中给出facenet.h5模型文件的获取与放置方式便于扩展训练集或二次开发。目前已有205人学习下载代码经测试运行成功答辩评审平均分达96分适合需要快速搭建人脸口罩识别原型、完成课程设计或毕业设计初期的在校生与开发者参考。1. Faster RCNN 人脸口罩识别系统这套课程设计源码能直接跑通到什么程度如果你正在为“人脸口罩识别”课程设计发愁先说结论这套资源不是只有模型的半成品而是把 python 源码、运行说明、数据集、模型四部分都打包好了。解压后跟着说明装好依赖先拿预训练权重跑一次推理再跑训练脚本整个流程是完整的。它用 Faster RCNN 做检测框架用 ResNet50 提取特征区分戴口罩和不戴口罩两类人脸区域和课程设计里“先检测人脸区域、再判断是否佩戴口罩”的常见写法一致。适合刚学完 CNN、想在目标检测上做完整项目的人也适合时间紧、需要先出可复现结果再补分析的同学。下面按我拆项目的顺序讲文件结构、训练参数、推理脚本和最容易踩的坑。2. 模块拆解与数据集准备从压缩包目录到第一批训练样本2.1 为什么口罩检测用 Faster RCNN 而不是 YOLO 或 SSDFaster RCNN 是 two-stage 检测器先用 RPNRegion Proposal Network生成候选区域再对每个候选区域做 RoI 特征提取和分类回归。口罩检测场景里目标尺度不大不小而且存在大量“口罩戴得偏低、鼻子露出来、口罩被手遮挡”这类难例two-stage 方法在这种数据上的精度通常比 one-stage 方法更稳。课程设计答辩时RPN 候选框加分类头这个结构也比 YOLO 的网格划分更容易讲透原理图和损失函数都能写出一小节。选择 Faster RCNN 还有一个很现实的原因这套源码配套的数据集大多是 VOC 格式的 XML 标注torchvision 的 Faster RCNN 数据接口对 VOC 和 COCO 都友好。YOLO 和 SSD 需要把标注转换成 txt 格式多一层转换也多个出错点。在几千张图片的小数据集上Faster RCNN 用 COCO 预训练权重做微调收敛速度通常比从零训练一个 YOLO 更快跑出来的效果也更符合“课程设计要能演示”的预期。2.2 解压后的目录结构与源码对应关系一般拿到手的压缩包解压后会看到类似下面这样的结构路径作用data/JPEGImagesVOC 原图目录data/AnnotationsVOC XML 标注目录data/ImageSets/Maintrain.txt、val.txt 等划分文件models/训练好的 .pth 模型权重src/dataset.py读取 XML 和图片生成训练样本src/train.py训练入口脚本src/predict.py推理测试脚本run.md运行说明环境依赖和操作顺序requirements.txtpip 依赖清单我拿到压缩包后习惯先看两个文件run.md 和 labels.txt。run.md 会写清楚依赖版本和训练顺序labels.txt 则直接决定类别 ID 映射后面训练脚本里的num_classes就是按它来的。2.3 把散装口罩图片整理成 VOC 格式有些版本给的数据不是现成 VOC 结构而是原始图片加同名 XML 放在一个文件夹里。这时候需要先整理一下import glob import os import shutil raw_dir raw # 原图 同名 xml 的目录 voc_img_dir data/JPEGImages voc_anno_dir data/Annotations os.makedirs(voc_img_dir, exist_okTrue) os.makedirs(voc_anno_dir, exist_okTrue) for xml_path in glob.glob(os.path.join(raw_dir, *.xml)): base os.path.splitext(os.path.basename(xml_path))[0] img_path os.path.join(raw_dir, base .jpg) if not os.path.exists(img_path): print(missing image:, img_path) continue # 统一命名成 mask_000001.jpg避免文件名带中文或空格 new_base mask_ base.zfill(6) shutil.copy(img_path, os.path.join(voc_img_dir, new_base .jpg)) shutil.copy(xml_path, os.path.join(voc_anno_dir, new_base .xml))这段脚本做的事情很简单把散装图片和同名 XML 复制到 VOC 目录同时统一命名。base.zfill(6)的作用是把1补成000001避免后续训练时文件名排序错乱。如果图片格式是 png 或 JPEG记得把img_path里的后缀改成对应格式。2.4 类别映射与 class id 对齐labels.txt 里一般是两行mask no_mask训练脚本加载时会把第一行映射成 0第二行映射成 1。数据集的 XML 里namemask/name对应标签 0nameno_mask/name对应标签 1。这里是最容易翻车的地方如果 labels.txt 顺序和读取代码不一致模型训练时相当于把两个类别的标签调换了训练过程看起来正常但预测结果全反。拿到数据后我建议先写个几行的检查脚本随便抽 10 张图打印每张图的target[labels]和target[boxes]确认 0、1 的分布和标注内容对得上。这一步五分钟能做完能省掉后面一整天的排查时间。3. 训练参数与模型保存让 Faster RCNN 在口罩这类目标上真正收敛3.1 ResNet50FPN 的默认配置为什么够用torchvision 里fasterrcnn_resnet50_fpn是默认组合ResNet50 作为 backbone加上 FPN 多尺度特征金字塔。口罩在画面里的占比通常是 10% 到 30%属于中尺度目标FPN 的 P3、P4 层已经能提供足够丰富的语义信息不需要换成 ResNet101。ResNet101 的显存占用几乎翻倍训练时间也明显变长在几千张图的小数据集上收益往往只有零点几个 mAP 点课程设计里没必要追求这个。如果训练样本里大量是小脸远景图例如集体照、监控截图这时候才考虑用 ResNet101 或把输入图放大。否则默认配置足够把精力放在数据增强和 anchor 调整上更划算。3.2 anchor scale 和 aspect ratio 要不要改torchvision 默认的 RPN anchor 参数是sizes(32, 64, 128, 256, 512)aspect_ratios(0.5, 1.0, 2.0)。对“戴口罩的人脸”这个场景人脸检测框的宽高比一般在 0.6 到 1.5 之间默认的 1.0 和 0.5 已经覆盖了大部分情况不需要大改。真正需要改的是小目标占比高的数据集。如果你发现标注框普遍小于 32 像素可以给最小的 FPN 层加一组更小的 anchorfrom torchvision.models.detection.rpn import AnchorGenerator anchor_generator AnchorGenerator( sizes((16,), (32,), (64,), (128,), (256,)), aspect_ratios((0.5, 1.0, 2.0),) * 5 ) model.rpn.anchor_generator anchor_generator注意sizes里必须写 5 个元素因为 FPN 有 P2 到 P5 共 5 层特征图每个元素对应一层。aspect_ratios写成 5 组是为了保持每组维度一致否则会报维度错误。改完 anchor 后 RPN 的 NMS 阈值和 proposal 数量不需要动默认值在普通数据集上表现稳定。3.3 学习率、batch size、epoch 设置这套源码常见的训练命令是这样python src/train.py --data data --epochs 24 --batch-size 2 --lr 0.005batch_size2是因为 Faster RCNN 训练时显存占用很高一张 800x1000 的图在 batch 2 下就要占用约 8GB 显存。显存不够就先降到 1不要硬开大 batch。lr0.005是 torchvision 在 VOC 上的常用初始值配合 SGD 的 momentum 0.9 和 weight_decay 0.0005 使用。如果 batch size 降到 1学习率建议同步降到 0.001 或 0.002否则 loss 在开头几百个 iter 里会剧烈震荡。训练脚本里的典型配置段大致长这样num_classes 2 batch_size 2 epochs 24 lr 0.005 momentum 0.9 weight_decay 0.0005 lr_steps [16, 22] # 第 16 和第 22 个 epoch 后学习率衰减 warmup_iters 500 # 前 500 个 iter 做线性预热lr_steps[16, 22]的意思是前 16 个 epoch 保持初始学习率第 16 个 epoch 后衰减 0.1 倍第 22 个 epoch 后再衰减 0.1 倍。这种阶梯式衰减比余弦衰减更好解释课程设计报告里可以写“采用阶梯式学习率衰减策略后期以更小步长逼近最优解”。3.4 断点续训与模型保存策略Faster RCNN 训练过程中 mAP 不是单调上升的中途经常出现几个 epoch 掉点再涨回来的情况。所以不要只保留最后一个 epoch 的权重最好每个 epoch 都保存一次再用验证集挑 besttorch.save(model.state_dict(), fcheckpoints/last_epoch{epoch}.pth) if val_loss best_loss: best_loss val_loss torch.save(model.state_dict(), checkpoints/best.pth)如果训练中断需要续训光保存model.state_dict()不够还要把 optimizer 和 lr_scheduler 一起保存torch.save({ model: model.state_dict(), optimizer: optimizer.state_dict(), lr_scheduler: lr_scheduler.state_dict(), epoch: epoch, }, checkpoints/resume.pth)恢复时先torch.load然后分别加载。只恢复模型权重会导致 optimizer 里的 momentum 状态丢失重新训练的前几百个 iter 学习率波动会特别厉害loss 可能突然升高。4. 推理与结果验证从 pth 权重到单张图和批量测试4.1 单张图片检测脚本训练完成后最先要验证的是单个图片的检测效果。我一般用一个独立脚本predict.py逻辑如下import cv2 import torch from torchvision.models.detection import fasterrcnn_resnet50_fpn model fasterrcnn_resnet50_fpn(pretrainedFalse) model.load_state_dict(torch.load(models/FasterRCNN_resnet50.pth, map_locationcpu)) model.eval() img cv2.imread(test.jpg) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) tensor torch.from_numpy(img_rgb / 255.0).permute(2, 0, 1).float() with torch.no_grad(): pred model([tensor])[0] for box, label, score in zip(pred[boxes], pred[labels], pred[scores]): if score 0.5: continue x1, y1, x2, y2 box.int().tolist() print(flabel{label}, score{score:.3f}, box{x1},{y1},{x2},{y2})几个细节解释一下。pretrainedFalse是为了避免加载模型时再次下载 COCO 预训练权重我们要用的是压缩包自带的权重。map_locationcpu可以让你在没有 GPU 的机器上先测试权重是否完整跑通了再放到 GPU 上推理。输入 tensor 的维度顺序必须是[C, H, W]而且通道顺序是 RGB直接用 BGR 喂进去会得到错误预测。4.2 批量跑测试集并统计 mAP课程设计通常需要给出一个量化指标mAP 是最常被要求的。批量推理时先把所有预测结果保存成 COCO 格式的 JSON再用 pycocotools 计算import json results [] for image_id, img in enumerate(test_images): pred model([img])[0] for box, label, score in zip(pred[boxes], pred[labels], pred[scores]): if score 0.05: continue x1, y1, x2, y2 box.tolist() results.append({ image_id: image_id, category_id: int(label), bbox: [x1, y1, x2 - x1, y2 - y1], score: float(score), }) json.dump(results, open(predictions.json, w))这里评估时阈值设 0.05不是 0.5。COCO 的 mAP 计算逻辑是先把所有框按 score 排序从高到低逐渐加入每个阈值点都算一次 precision 和 recall最后取平均。如果一开始就用 0.5 过滤低置信度的正确预测框会被提前丢掉mAP 反而偏低。4.3 保存带框结果图并导出表演示时需要可视化结果我习惯把检测框画在图上保存同时导出一个 CSV 方便写报告for img_path in test_list: img cv2.imread(img_path) pred model(transform(img))[0] for box, label, score in zip(pred[boxes], pred[labels], pred[scores]): if score 0.5: continue x1, y1, x2, y2 box.int().tolist() cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, f{int(label)} {score:.2f}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(output_ os.path.basename(img_path), img)CSV 导出用csv.writer写文件名、类别、置信度和坐标即可。这一步主要是为了让答辩老师直接看效果图而不是只看一个 mAP 数字。5. 实践避坑口罩检测训练里最常踩的五个问题与排查清单5.1 训练 20 个 epochmAP 还是 0现象训练 loss 在下降但 predict 时一张图也检测不到mAP 一直是 0。原因最常见的是类别 ID 与标签读取顺序错乱。模型把“口罩”当成“无口罩”两个类别互相干扰分类器输出概率接近随机。另一种可能是一开始就把 BGR 图像喂进了网络特征提取层学到的是错误的颜色分布。解决第一步检查 labels.txt 顺序和 dataset.py 里的 class 映射是否一致打印 10 张图的 target 确认标签值。第二步检查输入图像通道顺序统一使用cv2.cvtColor(img, cv2.COLOR_BGR2RGB)转换。5.2 预测框全部框在脸的下半部分或一个框圈住整张脸现象模型能检测到人脸位置但框的定位明显偏下或者把整张脸连带头发一起框进去。原因anchor 宽高比与真实目标不匹配RPN 回归压力太大。还有一种情况是标注本身不统一有的框只包住口罩区域有的框包住整张脸模型在训练时学习的几何范围是两套标准。解决先用脚本来统计所有标注框的宽高比分布再按统计结果调整aspect_ratios。如果数据里确实混了两种标注标准建议把标注统一成“包住整个人脸区域”而不是只包口罩区域这样分类头的学习目标更稳定。5.3 一开训练就 OOM现象CUDA out of memorybatch size 从 2 改成 1 仍然报错。原因Faster RCNN 训练时显存占用包含 backbone 特征图、RPN 中间结果、RoI 特征和分类头梯度。如果输入图片没有缩放原图 4000x3000 直接进网络P2 层的特征图会被撑得非常大显存瞬间爆掉。解决在数据加载的 transform 里把图片最长边缩放到 1333最短边限制在 800 以内。如果还 OOM把max_size降到 1000batch 取 1。这个操作同时能提升小数据集上的收敛稳定性因为大图上很多小目标其实是无效信息。5.4 xml 和 jpg 文件名对不上现象数据加载时报FileNotFoundError或训练过程中某些图被重复读取一个 epoch。原因原始图片命名是中文或带空格整理脚本没有做统一重命名VOC 的 ImageSets 文件里写了带.jpg后缀的名字而 dataset 拼接路径时又加了一次后缀导致路径重复。解决统一重命名成mask_000001.jpg这种纯英文编号格式。ImageSets 里的 txt 只写文件名前缀不带扩展名。在 dataset 初始化时统计一次图片数和标注数不一致直接打印缺失名单不要等到训练中途才报错。5.5 测试集 AP 高摄像头实拍漏检多现象验证集 mAP 0.9 以上测试集效果不错但拿手机拍一张暗光照片要么漏检要么把围巾、黑色口罩当成无口罩。原因公开口罩数据集里正脸、光线好的图片占绝大多数模型没见过侧脸、低头、逆光和遮挡样本。颜色单一的蓝色口罩也容易让模型把颜色当成主要判据。解决训练时打开随机翻转和 HSV 抖动增强让模型对颜色变化不敏感。再从自己实际使用场景里收集 50 到 100 张图片加入训练集这种针对性补充比增加增强强度更有效。6. 进阶用法基于现有模型做迁移学习微调与速度验证6.1 用你自己的图片微调模型拿到模型权重后第一件值得做的事是迁移学习微调。不用从头训练直接加载给出的 Faster RCNN 权重替换最后的预测器from torchvision.models.detection import fasterrcnn_resnet50_fpn from torchvision.models.detection.faster_rcnn import FastRCNNPredictor model fasterrcnn_resnet50_fpn(pretrainedFalse) model.load_state_dict(torch.load(models/FasterRCNN_resnet50.pth, map_locationcpu)) in_features model.roi_heads.box_predictor.cls_score.in_features model.roi_heads.box_predictor FastRCNNPredictor(in_features, 2)in_features是现有分类头的输入维度通常是 1024。替换后整个模型只有最后一层被重置其余层的参数全部保留。如果自己数据还是 mask 和 no_mask 两类类别数不变替换 predictor 的目的是告诉 PyTorch 重新初始化分类头避免直接加载后训练不稳定。6.2 验证模型吞吐量课程设计报告里经常要写“推理速度”可以用一个简单的计时脚本拿到数字import time import torch model.eval() dummy torch.rand(1, 3, 800, 800) with torch.no_grad(): for _ in range(5): model(dummy) n 20 t0 time.time() for _ in range(n): model(dummy) fps n / (time.time() - t0) print(fFPS {fps:.2f})前 5 次推理是 warmup让 CUDA kernel 完成初始化不计入统计。后面 20 次推理取平均时间。CPU 环境下这个配置通常只有 1 到 3 FPSGPU 上能到 10 FPS 以上报告里注明测试环境即可。从那以后我每次拿到这类 Faster RCNN 课程设计源码都强制先做两件事先跑 predict.py 确认权重能正常出框再开 tensorboard 盯训练 loss。跑通了再改业务数据这套流程帮我避开了很多无谓的返工。希望帮到你。本文还有配套的精品资源点击获取