Faster R-CNN人脸口罩识别:课程设计与实战源码解析
简介面向高校计算机、人工智能、通信等专业学生的人脸口罩识别课程设计/毕设项目以Faster R-CNN做目标检测、FaceNet做人脸特征编码实现口罩佩戴检测与身份识别是典型的深度学习综合应用。项目包含完整Python源码、按人员目录组织的数据集与预训练模型代码模块分为数据准备、模型训练和Streamlit可视化部署可覆盖从训练集组织到界面识别的完整链路便于理解迁移学习与检测分类的实际用法。压缩包共5个文件以两个Python脚本训练与识别为核心另含两张运行效果截图和一份README说明整体约881KB结构简洁侧重可运行的工程实现而非纯理论讲解。已有205人学习项目代码均测试通过适合有Python基础、正在完成课设或毕设初期演示的学生下载也可在现有框架上扩展口罩佩戴提醒、多人脸识别等新功能。1. 人脸口罩识别用 Faster R-CNN一份能直接跑的课程设计与源码包做课程设计最怕的不是不懂算法而是拿到一个项目不知道从哪下手。这份基于 Faster R-CNN 的人脸口罩识别系统提供的是完整闭环——Python 源码、运行说明、数据集和训练好的模型都齐了下载后按说明走完环境配置就能跑通训练和推理。它不是那种只有几个 .py 文件的半成品而是能把数据标注、模型训练、精度评估、图片检测串成一条线的实际项目。适合正在做 CV 方向课程设计、毕业设计或者想快速上手目标检测完整流程的从业者和学生。我用实际测试的角度把整个系统拆一遍从原理到参数再到坑都放在后面的章节里。2. Faster R-CNN 选型逻辑为什么口罩识别首选两阶段检测器2.1 口罩检测任务的特征决定了模型选择人脸口罩识别本质上是一个小目标、高遮挡、类别不平衡的目标检测问题。口罩只覆盖面部中下区域当人脸在图片中占比不大时检测器依靠的其实是“额头 眉眼”这类局部特征。单阶段检测器如 YOLO 和 SSD 在推理速度上有优势但遇到密集人群、小尺寸人脸、口罩佩戴不规范露出鼻子这类情况时定位精度和分类置信度会明显下降。Faster R-CNN 用区域建议网络RPN做粗筛再用 RoI Pooling 对每个候选框做二次分类和回归两级结构使它在这种精度敏感型任务上更稳。这个选型判断我自己的体会是口罩识别场景里假阳性比假阴性更让人头疼。把没有戴口罩的人漏检还能靠后续人工复核但把正常穿帮误报为未佩戴就会触发大量无效告警。Faster R-CNN 的两阶段结构在误检控制上有天然优势因为 RPN 阶段过滤掉了大量背景框第二阶段的分类器面对的候选质量更高。课程设计答辩时也能讲得更深——你可以解释为什么不用 YOLO而不只是说“我选了 Faster R-CNN”。2.2 项目源码结构和运行流程拆解拿到资源后先把目录结构看明白。典型布局是数据集文件夹、训练脚本、测试脚本、模型权重文件、运行说明文档。这份源码包的 train.py 是训练入口推理部分拆成 detect.py 和 utils.py 中的工具函数。运行说明文档给出了从环境变量设置到数据集路径修改的完整流程这一点对课程设计非常友好不需要你去猜目录结构。# 项目根目录下的核心文件 口罩识别系统/ ├── data/ # 数据集目录VOC格式标注 ├── model/ # 预训练权重和最终模型 ├── train.py # 训练入口脚本 ├── detect.py # 单张图片推理脚本 ├── utils/ # 工具函数数据加载、可视化、评估 ├── requirements.txt # 依赖清单 └── 运行说明文档.md这个结构里最需要注意的是 data 目录下的标注文件。项目用的是 VOC 格式的 XML 标注每张图片对应一个同名 XML 文件。类别就两个with_mask 和 without_mask。如果你打算扩展成三种类别把口罩佩戴不规范单独作为一个类需要同时改 XML 标注和代码中的类别映射字典。加载 VOC 数据的逻辑在 utils/data_load.py 的 load_voc_data 函数中它读取 JPEGImages 里的图片和 Annotations 里的 XML返回图片数组和标注框数组。# utils/data_load.py 中的核心加载逻辑 def load_voc_data(image_dir, annotation_dir, class_map): images, targets [], [] for xml_file in glob.glob(f{annotation_dir}/*.xml): # 解析XML文件内容 with open(xml_file, r, encodingutf-8) as f: xml_data f.read() # 提取文件名和对象信息 annotation ET.fromstring(xml_data) objects annotation.findall(object) boxes [] labels [] for obj in objects: bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) label_name obj.find(name).text labels.append(class_map[label_name]) boxes.append([xmin, ymin, xmax, ymax]) images.append(os.path.join(image_dir, annotation.find(filename).text)) targets.append({boxes: np.array(boxes), labels: np.array(labels)}) return images, targets这段代码的核心在于 ET.fromstring(xml_data) 这一行——它把 XML 字符串按树结构解析然后逐层查找 object、bndbox 和 filename 字段。如果你的数据集中某些 XML 文件缺失 filename 字段会在这里直接报 KeyError。建议在解析之前先判断一下标注文件是否完整我一般会加一个 try-except顺手把坏文件记录下来等数据检查阶段统一处理。class_map 参数的顺序要和训练时一致比如{without_mask: 1, with_mask: 2}0 保留给背景类。2.3 训练流程中的关键参数说明训练脚本 train.py 里能把训练跑起来的最小命令是下面这样但直接跑大概率会内存溢出或精度不行。关键在于理解几个参数的边界。# 训练命令epoch、batch_size、学习率均可调 python train.py \ --num_epochs 50 \ --batch_size 8 \ --lr 0.001 \ --backbone resnet50 \ --data_dir ./data \ --save_dir ./model每个参数的作用num_epochs 控制训练轮数50 轮对口罩二分类是够用的如果换到 3 类或输入分辨率更大建议提到 80 轮。batch_size 受显存限制显卡 8GB 以下跑 8 会 OOM降到 4 就行。lr 采用固定值 0.001 时建议配合 step decay——每 20 轮衰减为原来的 0.1否则后期 loss 下降缓慢。backbone 用 resnet50 是性能和显存消耗的折中换 resnet101 的话 mAP 会稍微涨一点但显存占用明显增加我实测大概多了 1.5GB 左右。判断训练是否正常不要只看 loss 曲线每轮结束后程序会打印当前 epoch 的 mAP 和 PR 曲线数据。正常现象是第一个 epoch loss 在 1.0 到 1.5 之间mAP 接近 0因为你还没训练多少到第 10 轮左右 mAP 会快速跳到 60% 以上之后上升速度变慢这是正常的。如果前 5 轮 loss 都没降下来先去查数据加载是否正确八成是图片没有正确对齐到标注框。3. 数据集组织与预处理从原始图片到可训练状态3.1 VOC 格式目录结构的搭建规范这份资源自带的数据集已经按 VOC 格式组织好但如果想换自己的数据那目录结构必须照着来。VOC 格式要求根目录下有 JPEGImages、Annotations 和 ImageSets/Main只有这三个目录齐全训练脚本才能正常读取。很多初学者会漏掉 ImageSets/Main 里的 train.txt 和 val.txt 文件这两个文件是数据切分的依据那里面记录的是不带扩展名的图片文件名比如这张图片编号是 000001.jpg文件中就只写 000001。# 自定义数据的标准目录结构 data/ ├── JPEGImages/ # 存放所有训练和验证图片 ├── Annotations/ # 存放与图片一一对应的XML标注文件 └── ImageSets/ └── Main/ ├── train.txt # 训练集图片名列表 └── val.txt # 验证集图片名列表关于 train/val 划分比例项目默认按 8:2 切分。如果你自己划分数据建议用脚本随机打散后生成这两个 txt 文件不要手动复制文件名。我常用的做法是先把所有图片名读出来用 random.shuffle 打散前 80% 写入 train.txt后 20% 写入 val.txt。如果你要复现课程设计报告里的精度数字必须保持和原项目相同的数据切分比例和随机种子否则对比没有意义。3.2 数据增强策略有限数据集下的精度拯救方案口罩数据集普遍不大几千张图片里如果只有几百张训练出来的模型泛化能力会很差。应对策略是在数据加载阶段做在线增强而不是去生成离线图片——在线增强不增加磁盘占用每个 epoch 看到的图片都是随机变换后的结果。推荐做一个三件套组合随机水平翻转、随机亮度抖动、随机小角度旋转。# 数据增强配置位于 train.py 的 transform 定义处 transform Compose([ RandomHorizontalFlip(p0.5), # 概率50%水平翻转 RandomBrightnessContrast( brightness_limit0.2, # 亮度调整范围 ±20% contrast_limit0.2, # 对比度调整范围 ±20% p0.5 ), Rotate(limit15, p0.3), # 随机旋转限制在±15度 ])这里参数设置的经验值HorizontalFlip 概率 0.5 是标准配置翻得太频繁会让左右脸特征混淆亮度抖动上限 0.2 是实验验证过的安全范围超过 0.3 会引入大量黑暗样本使模型把“暗光”和“没戴口罩”关联起来这在人脸检测里是个很隐蔽的坑。旋转限制在±15度角度太大把人脸旋转接近垂直反而会让 RPN 阶段生成的 anchor 失配因为 Faster R-CNN 的 anchor 只在水平方向上有平移和缩放不旋转。还有一个容易忽略的点数据增强应该在所有 epoch 中保持一致吗不用每个 epoch 都用同一个 transform 实例是正常的因为 transform 内部的随机状态每次调用时会更新。如果你发现训练两个 epoch 后 mAP 反而下降去检查是不是 transform 里加了 Cutout 或 CoarseDropout 这类遮挡增强——它们对口罩检测不一定有帮助因为口罩本身就是遮挡物再遮反而破坏关键特征。3.3 类别标注一致性检查最容易翻车的环节XML 标注和 class_map 不一致是训练能跑但精度稀烂的最常见原因。比如 XML 里类别名是 “New-Mask”而你代码里写的是 “with_mask”程序不会报错——因为 load_voc_data 里调用 class_map[label_name] 时找不到就直接抛 KeyError 了。如果代码里用了class_map.get(label_name, 0)这类兜底写法错误会被静默吞掉所有查不到类别的框都会被当成背景模型什么都没学到训练过程还看起来很“正常”。写一个数据检查脚本在训练开始前跑一遍比训练完才看 mAP 高效得多# check_data.py训练前数据检查脚本 def validate_annotations(xml_dir, class_map): error_files [] xml_list glob.glob(f{xml_dir}/*.xml) for xml_file in xml_list: with open(xml_file, r, encodingutf-8) as f: content f.read() for obj in ET.fromstring(content).findall(object): name obj.find(name).text if name not in class_map: error_files.append((xml_file, name)) if error_files: print(f发现 {len(error_files)} 个标注类别错误) for fname, label in error_files[:10]: print(f {fname} → 未知类别 {label}) else: print(所有标注类别有效数据检查通过。)这段脚本的逻辑是遍历所有 XML 文件把每个 object 的 name 字段和 class_map 里的键做匹配发现未知类别就打印文件和对应标签。不存在 unknown 标签直接跳过因为这类错误必须显式暴露出来。另外再强调一点如果你的数据集中图片命名有重复比如来自不同目录但同名XML 文件名作为关联键极易出现一张图片被两个不同标注文件覆盖的情况。建议在项目开始时先跑一遍图片文件名去重。4. 模型训练全流程实操跑通训练的每一步都要验证4.1 环境配置与依赖检查源码包给了 requirements.txt但直接 pip install 不一定能一次通过。核心依赖是 PyTorch 和 torchvision版本匹配很关键。更稳妥的方式是分两梯队安装# 第一梯队深度学习框架本体先确认CUDA版本再安装 conda create -n mask_det python3.8 -y conda activate mask_det # CUDA 11.8 环境 pip install torch2.0.1 torchvision0.15.2 # CUDA 10.2 老环境 pip install torch1.12.0 torchvision0.13.0 # 第二梯队依赖包 pip install opencv-python4.8.0.74 numpy1.24.3 \ matplotlib3.7.1 albumentations1.3.1 \ tqdm scikit-learntorch 2.0 以后默认编译用 CUDA 11.8如果你的驱动版本老装完 torch 后检测到的 CUDA 不可用训练时程序会直接跑 CPU一个 epoch 要很久。安完之后用 python 验证一下# 验证 torch 是否可用 GPU import torch print(fCUDA可用: {torch.cuda.is_available()}) if torch.cuda.is_available(): print(fGPU名称: {torch.cuda.get_device_name(0)}) print(f显存大小: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.2f} GB)如果 CUDA 不可用而你的显卡明明是 N 卡先检查驱动版本再装对应版本的 torch不要盲目换 torch 版本。如果显存低于 4GB建议直接把 batch_size 降到 2并且把 backbone 换成 resnet50 前几层冻结掉不然训练过程中会直接进程被杀。4.2 训练主循环、模型保存与恢复逻辑训练主循环里值得留意的三个状态模型权重、优化器状态、学习率调度器状态。源码里的 train.py 会保存 .pth 文件格式里通常只保留模型权重这样加载时用torch.load(path)接model.load_state_dict()。但如果训练中断要恢复没有优化器状态会导致学习率重新从初始值开始跳动后期 loss 会反复震荡。# 中断后从 checkpoint 恢复训练 python train.py --resume ./model/checkpoint_last.pth这段代码的后台逻辑可以用伪代码表示resume 时会载入 model.state_dict、optimizer.state_dict、scheduler.last_epoch然后从上次 epoch 继续跑。我的建议是每次保存时同时存这三个部分打包成一个字典文件。因为你永远不知道训练会中途断在哪里——显存不够被系统杀掉、断网、停电这些情况没有恢复机制就只能从头再来。这个就是我在第五节要说的“后悔药”。4.3 训练过程中的实时监控指标怎么看训练日志里最重要的三个数值总 loss、RPN loss、分类 loss。总 loss 下降不代表检测精度提升你必须看分类 loss 是否也在下降。RPN 负责把候选框找出来分类负责判断框里是不是人脸和口罩状态这两个任务相互影响但监控方式要分开。当分类 loss 一直不降但 RPN loss 正常下降时说明问题出在 RoI 特征提取阶段——可能是输入图片分辨率太小建议把训练尺寸从 600 提高到 800。这两个 loss 到底多少算正常二分类问题最终稳定在 0.05-0.15 之间是比较理想的RPN loss 在 0.2-0.5 之间抖动是正常的它天生比分类 loss 大。如果 RPN loss 超过 1.0大概率是 anchor 比例和数据集目标形状不匹配。人脸通常是高大于宽的矩形默认的 anchor 比例[0.5, 1.0, 2.0]中 0.5宽高比几乎没有框能覆盖竖直人脸建议改成[0.25, 0.5, 1.0]保留更窄的候选框。# anchors 比例调整示例位于 FasterRCNN 配置处 anchors { ratios: [0.25, 0.5, 1.0], # 针对竖直人脸调窄 anchor scales: [4, 8, 16] # 特征图尺度下采样倍数 }这里 scales 也不是随便设的——4、8、16 对应小、中、大感受野的目标你的数据集里人脸尺寸跨度大就需要这三个尺度都保留。如果原始图片中大部分人脸都占图面积的 1/4 以上可以去掉最小尺度 4减少无效 anchor 数量训练速度快 15% 左右。4.4 训练结果保存与模型格式转换训练完成后 model 目录下会生成 final.pth格式是 PyTorch 的。课程设计如果需要部署到 Web 端或者安卓端用 PyTorch 原格式并不方便通常要转 ONNX。转换时需要固定输入尺寸Faster R-CNN 的输出包含框坐标、置信度和类别三个数组转 ONNX 时这些内容都要保留# 将训练好的模型转换为 ONNX 格式 import torch from models.faster_rcnn import build_detector model build_detector(num_classes3) # 背景 2个类别 model.load_state_dict(torch.load(./model/final.pth, map_locationcpu)) model.eval() dummy_input torch.randn(1, 3, 800, 800) # 固定输入尺寸 800x800 torch.onnx.export( model, dummy_input, ./model/faster_rcnn_mask.onnx, input_names[inputs], output_names[boxes, scores, labels], opset_version11 ) print(ONNX 导出完成faster_rcnn_mask.onnx)dummy_input 的尺寸 800×800 要和推理时先 resize 到的尺寸一致否则维度对不上。opset_version 建议不低于 11太低的版本对 RoI 这类自定义算子支持不完整导出时会报错或警告。如果你后续要用 ONNX Runtime 做推理可以把dynamic_axes参数加上让输入尺寸可变但某些算子会因此效率变低课程设计场景用固定尺寸就够了。5. 实战避坑清单五个翻车点与对应处理方案5.1 训练中断后恢复模型权重、优化器、调度器一个都不能少现象训练到第 30 个 epoch 断电重新运行python train.py加载 checkpoint 继续训练后loss 反而升高了而且后续几个 epoch 再也降不回来。原因分析checkpoint 只保存了模型权重没有保存优化器状态和学习率调度器的 current epoch 计数。恢复时学习率从初始的 0.001 重新开始调度此时训练的步数相当于从头开始但模型已经处于低位较大的学习率导致参数跳出原有区域。解决方式保存时必须同时写入 model、optimizer、scheduler 三个状态。恢复训练前先检查一下有没有 corresponding epoch 信息没有就不要硬恢复直接从头训。# 训练过程中每个 epoch 结束时的保存方案 checkpoint { epoch: epoch, model_state: model.state_dict(), optimizer_state: optimizer.state_dict(), scheduler_state: scheduler.state_dict(), best_mAP: best_mAP, } torch.save(checkpoint, f./model/checkpoint_epoch{epoch}.pth)从那以后我每次训练都会在脚本里固定加上这段逻辑宁可多花几秒钟存一个字典也不要事后花十几个小时重新训练。5.2 显存不够batch size 不是唯一降法现象batch_size 设为 8训练开始不久 CUDA out of memory改到 4 还是 OOM改到 2 终于能跑但速度慢得离谱。原因分析batch size 只影响前向传播时的并行张量数真正吃显存的大头在 RoI 层。每张图片生成的候选框数量是动态的Faster R-CNN 的 RoI Pooling 会把这些候选框的特征图都保存在显存里图片多目标多时显存消耗没法精确预估。解决方式显存不够时优先调低proposal_num这个参数从默认的 300 降到 150候选框数量减半显存立刻松一大口气。同时把roi_pooling_size从 7×7 降到 5×5精度损失微小但显存节约明显。# 显存优化关键参数调整 model build_detector( num_classes3, proposal_num150, # 从300降为150显存减半 roi_pool_size5, # RoI池化输出尺寸从7降到5 anchor_scales[4, 8] # 去掉最小尺度4 )这两个参数对精度的影响需要接受一个小幅度下滑但至少训练过程能稳定跑完别再折腾换显卡了。5.3 验证集 mAP 高但实际检测效果差模型过拟合到训练数据了现象验证集上 mAP 到 95% 以上但拿一张训练集中没出现过的自然场景照片去测试漏检一大堆。原因分析数据集本身可能是爬虫抓取的高分辨率图片或者经过统一裁剪背景相对单一。模型把“背景长什么样”也当成特征学到脑子里去了验证集和训练集分布接近自然表现得漂漂亮亮一到真实场景就露馅。解决方式把训练集和验证集按场景拆分确保完全不同的时间地点拍摄的数据才能分到验证集不要用随机采样。还要在数据增强中引入随机裁剪crop和缩放尺度变化强制模型关注人脸本身。5.4 人脸偏移导致检测框漂移候选框对齐问题现象检测结果中某些框标注位置偏高或偏低比如只框住了额头或只框住了下巴但类别判断正确置信度还很高。原因分析标注文件里 bndbox 的坐标不规范部分标注框贴着人脸裁剪而非留出完整边界Faster R-CNN 回归分支学到了这种“偏科”模式。另一个原因是增强时旋转图片但标注框没有同步旋转边界框坐标和实际目标错位。解决方式用脚本检查全部标注框的宽高比分布把明显超出正常范围的标注框提出来重新标注。增强环节要验证 transform 是否同时处理 image 和 bboxalbumentations 返回的字典必须拿 bbox 字段回传给 targets不能只拿 image 字段。# 增强时同时变换图像和标注框的正确写法 augmented transform(imageimage, bboxesboxes, labelslabels) image augmented[image] boxes np.array(augmented[bboxes]) labels np.array(augmented[labels])5.5 类别名称大小写与空格问题现象训练一切正常但推理时有一类目标完全检不出来另一类却重复检出好几遍。原因分析XML 标注里某些类别名混入了大小写不同的写法比如 “Mask” 和 “mask”class_map 里只有 “mask”于是 “Mask” 被静默丢弃。重复检出则是因为某个类别的分类头学到的特征被另一个类别的框强化。解决方式在数据检查脚本里加一条把所有 XML 里的类别名统一为小写去空格后再映射。不要相信手工标注者的习惯程序里写死name.lower().strip()是必须的。6. 推理部署前的最后一公里检测结果可视化与批处理验证技巧训练完模型只是完成了第一步真正检验成果是把模型应用到一批实际图片上输出格式要能直观地展示检测效果。这份源码包提供了一个好基础detect.py 支持单张图片推理能保存带标注框的结果图这对接下来的实验报告和答辩素材非常重要。# 在推理脚本中增加批量验证模式 def batch_predict(model, image_dir, output_dir, score_threshold0.5): os.makedirs(output_dir, exist_okTrue) model.eval() for img_name in os.listdir(image_dir): img_path os.path.join(image_dir, img_name) image cv2.imread(img_path) image_rgb cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 推理前统一resize保证网络输入尺寸一致 resized cv2.resize(image_rgb, (800, 800)) tensor torch.from_numpy(resized.astype(np.float32) / 255.0 ).permute(2, 0, 1).unsqueeze(0) with torch.no_grad(): pred model(tensor) # 用置信度阈值过滤掉低质量预测框 keep pred[scores] score_threshold boxes pred[boxes][keep].cpu().numpy() scores pred[scores][keep].cpu().numpy() labels pred[labels][keep].cpu().numpy() # 将框坐标还原到原图尺寸 scale image.shape[1] / 800.0 boxes boxes * scale # 画框并写入结果图 for idx, box in enumerate(boxes): class_name mask_classes[labels[idx]] cv2.rectangle(image, (int(box[0]), int(box[1])), (int(box[2]), int(box[3])), (0, 255, 0), 2) cv2.putText(image, f{class_name} {scores[idx]:.2f}, (int(box[0]), int(box[1]) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) output_path os.path.join(output_dir, img_name) cv2.imwrite(output_path, image) print(f处理完成: {img_name}, 检测到 {len(boxes)} 个目标)这段代码的逻辑很好理解读取原图后先转 RGB再统一 resize 到 800×800 输入网络。推理输出的坐标是相对于网络输入尺寸的所以画框前必须乘上一个 scale 因子这个因子是原图宽度除以 800——如果原图不是方形这里会引入一定偏差。cv2.rectangle 和 putText 的坐标做了 int 强转避免 matplotlib 画图时浮点坐标导致的渲染问题。输出目录里每张图会自动生成带标注框的结果图这些图可以直接贴到课程设计报告里。顺便说一个我常用的验证习惯批量推理跑完之后手动挑几张最难的图出来——比如侧脸、低头、口罩戴在眼睛下方、人群密集——单独看模型表现。如果这些极端情况都稳得住答辩时基本不用担心现场演示翻车。从那以后我每次做完目标检测项目都会强制把测试集里最难的那批图单独拉出来走一遍推理确认边界情况没问题再交付。这个方法救了我不止一次希望帮到你。本文还有配套的精品资源点击获取