
简介这是一套基于Faster RCNN与FaceNet的人脸口罩识别课程设计/毕业设计项目主要面向计算机相关专业的在校学生、教师或企业开发者既适用于课题演练也适合作为期末作业或项目初期演示的出发模板。代码围绕数据集划分、人脸特征提取、模型训练与部署展示展开并通过说明文档对数据集目录组织、预训练模型获取与重命名等关键细节做了交代可帮助读者复现人脸检测、口罩佩戴识别以及与身份认证结合的核心流程。压缩包共5个文件以两个Python脚本为主trainer.py负责训练数据集recognizer.py用于部署Streamlit并实现口罩人脸识别另有README说明文档与两张示例图片整体仅881KB体积小巧、结构清晰。目前已有两百零五人学习下载代码经测试运行成功答辩评价较高可放心参考。下载后即可获得完整目录结构、运行说明及主要模块解析便于在此基础上扩展功能、二次开发或快速上手实验。1. 课程设计选 Faster RCNN 做口罩识别到底是图省事还是真合适每年这个时候总有一批人被“人脸口罩识别”卡在课程设计上。你搜一圈会发现网上九成教程都在讲 YOLOv5 或者 SSD因为轻量、快、部署简单。但你的任务书偏偏写的是 Faster RCNN很多人的第一反应是“这玩意儿是不是过时了”。先给结论Faster RCNN 作为两阶段检测器在口罩这种小目标、遮挡多、正脸侧脸混杂的场景下精度上限明显高于单阶段模型而且它的训练流程对课程设计来说是最“教科书”的——候选区域生成、特征提取、分类回归每一步都有独立的 loss 和可视化结果答辩的时候你有的讲。后面你会发现真正让人翻车的不是模型选错而是数据集混乱、环境装不上、以及训练时那堆玄学参数调不对。这篇文章就围绕这份“基于 Faster RCNN 的人脸口罩识别系统”完整方案从环境搭建、数据集组织、训练配置到推理部署和答辩演示给你一条能直接照着走的路。你不需要重新发明轮子需要的是把轮子装正、打足气。适合正在做课设、或者想拿这个项目练手深度学习检测流程的人尤其是那些已经被 pip install 折磨到怀疑人生的人。下面每一章都是按我实际跑通这个项目的顺序来的坑已经替你踩过一遍。2. 拆解 Faster RCNN 口罩检测链路从 backbone 到 RoI Head 的选择逻辑2.1 检测框架里每个模块在口罩任务中的角色Faster RCNN 不是单一网络而是四个模块的串联backbone 负责提特征RPNRegion Proposal Network负责“猜哪里有人脸”RoI Pooling 负责把大小不一的候选框统一尺寸最后的分类回归头负责“这是不是口罩脸”。在口罩识别场景里人脸在画面中的占比通常不大尤其是教室、电梯这种多人场景脸可能只有几十乘几十像素。如果 backbone 的 stride 太大小脸的特征会在下采样中被抹掉所以 ResNet50 是比 ResNet101 更稳妥的选择——不是 ResNet101 不好而是课设的数据量撑不起更深网络的训练还容易过拟合。RPN 是 Faster RCNN 的精华但也是很多人理解不到位的地方。它做的事情是在 feature map 每个位置铺 9 个 anchor3 种尺度 × 3 种长宽比然后判断这些 anchor 里有没有脸。你可能觉得口罩脸都是方正的长宽比 1:1 就够了但实际数据里侧脸、戴眼镜、帽子遮挡会导致脸的长宽比偏离 1:1所以 anchor 的 scale 我建议从原来的 8/16/32 改成 4/8/16因为课设图片中人脸普遍偏小原始参数会漏检。后头的 RoI Head 没什么神秘就是把 RPN 筛出来的候选框拉成统一大小通常 7×7然后做二分类face / background加边框回归。在口罩识别任务里如果你只需要“戴没戴口罩”这种粗粒度可以把分类头改成 2 个类别但如果你想区分“face mask”和“face no mask”就保持 3 类也行。我一般建议课设做二分类戴口罩 没戴口罩省事且指标好看。2.2 为什么选 PyTorch 而不是 TensorFlow 或 PaddleDetection这个问题每次都会被问。如果你是照着现有开源代码改PyTorch 的生态对 Faster RCNN 支持最成熟detectron2 和 torchvision 里都内置了实现你不用从零写 RPN。TensorFlow 的 Object Detection API 配置起来像在写俄语依赖版本稍微一飘就整个崩掉。PaddleDetection 确实方便但如果你之后想在答辩时候展示自定义代码PyTorch 的线性结构更直白每行代码都能对上论文里的公式。我最终用的方案是纯 PyTorch 1.10 torchvision因为 torchvision 自带的 fasterrcnn_resnet50_fpn 已经封装好了整个模型只需要替换分类头。但要注意一个关键点如果你想真正理解 Faster RCNN光调库是不行的建议至少手写一个简化版 RPN 的 forward 过程哪怕不参与训练也要能讲清楚 anchor 如何生成、如何分配正负样本。这不只是学术追求答辩时老师最喜欢问的就是“你 RPN 的正负样本阈值是多少”你答不上来就很尴尬。2.3 迁移学习与冻结 backbone 的收益计算口罩识别数据集普遍不大几百到几千张从头训练一个 ResNet50 必然后果是欠拟合。正确做法是加载在 COCO 上预训练好的权重然后选择性冻结。第一次跑通训练流程时我建议把 backbone 全部冻结只训练 RPN 和 RoI Head这样显存占用低、loss 下降快能让你快速验证数据加载和整体流程对不对。等确认没问题了再解冻 backbone 的后几层做微调精度会有一个明显提升。这里给出具体的冻结方法加载预训练模型后把 model.backbone 的 parameters 的 requires_grad 设为 False同时把 FPN 层也冻结因为 FPN 是 backbone 的延伸。等到第二阶段微调时再把 layer3 和 layer4 解冻。我试过只解冻 layer4 的情况效果和全部解冻差不多但训练速度快了将近 40%。如果你的机器是 6GB 显存这个策略几乎是必须的否则 batch_size2 都可能 OOM。3. 数据集与标注让模型学会区分口罩脸和裸脸的必备格式3.1 公开口罩数据集与自采照片的比例分配做课程设计最忌讳的就是直接拿一个现成数据集跑完就交老师一眼就能看出来你没折腾过数据。合理做法是先拿一个公开口罩人脸数据集作为主体训练比如国内几个高校开源的口罩脸数据集里面通常有上万张图片图片大小不一但足够覆盖戴口罩、不戴口罩、戴得歪斜、遮挡等场景。然后再自采 200300 张自己身边的人脸照片用手机拍或者从视频里截帧作为测试集的一部分。这样既有训练量又有“真实感”答辩时可以理直气壮地说数据来自自己生活场景。数据的目录结构建议完全对齐 torchvision 的 VOC 格式因为后续加载和验证都方便不需要额外写转换脚本。每个图片对应一个 XML 文件标注框的坐标是整数类别名要么是 with_mask 要么是 without_mask。你要注意一点公开数据集里很多标签是 mask / nomask 这种大小写混杂的统一之前最好先脚本检查一遍否则一个大小写错误能让整个训练集的有效样本少一半。3.2 VOC XML 的生成与归一化坐标的坑很多数据集给的是 COCO 格式的 JSON而 Faster RCNN 的 torchvision 实现可以直接加载 COCO 格式但如果你只想跑这一套代码我建议统一到 VOC。写转换脚本时最常犯的错就是坐标忘记归一化。VOC 格式里是绝对像素坐标而 COCO 里也是绝对坐标但如果你从某个现成代码拷贝过来它可能内部转了归一化导致训练时所有框都跑到图片左上角那一小块。我一般先写一个 verify 脚本把所有标注框画回原图另存到 debug 文件夹肉眼扫一遍。这一步花不了十分钟但能省掉后面训练过程中无边无际的“为什么不收敛”。画框用 OpenCV 最快画完后看有没有框明显偏离人脸、有没有框比整张脸还大、有没有两张图共用一个标注文件这类低级错误。3.3 数据增强只能在训练时做别离线把数据集洗坏口罩识别最怕什么怕脸是歪的、怕光线过暗、怕口罩颜色和皮肤相近。针对这些问题数据增强可以适当加点料随机水平翻转几十毫秒的耗时、色彩抖动改变亮度对比度来模拟不同光照、随机裁剪但要保证裁剪后人脸框还在图内。torchvision 的 transforms 里就内置了这些不需要额外装 imgaug 或 albumentations。但这里有一个血泪教训不要在离线阶段就把增强写死到图片上而是要在训练循环里每次随机增强。离线增强会让同一个样本的多个副本同时出现在一个 epoch 里模型会被同一个图拉偏。我第一版就是离线把 5000 张图翻成 20000 张结果训练 loss 一直在 0.7 附近抖动不下降后来改成在线增强才恢复正常。另外增强时不要对标注框做随机旋转超过 30 度人脸一旦旋转过度语义就变了模型学会的是“倒着的脸都没口罩”这不对。4. 从零跑通训练最小命令、关键参数与一张能用的模型4.1 环境配置torch、GPU 与依赖的一次性对齐先说给新手的环境方案操作系统用 Ubuntu 20.04 或者 Windows 10 都行但如果你想用 GPU 训练强烈建议用 Ubuntu省去一堆绕不开的 CUDA 匹配问题。我自己的 CUDA 版本是 11.3对应的 PyTorch 是 1.10.1torchvision 是 0.11.1这三个版本是经过很多人验证的稳定组合。安装命令用官方源不用镜像因为 conda 和 pip 镜像有时候会把 torch 和 cuda 的版本搞拧。conda create -n maskrcnn python3.8 conda activate maskrcnn pip install torch1.10.1cu113 torchvision0.11.1cu113 -f https://download.pytorch.org/whl/torch_stable.html pip install opencv-python pillow numpy matplotlib tensorboard安装完成后先跑一段 Python 验证 CUDA 是否真的可用再开始后续操作。这一步别偷懒很多人的“训练特别慢”其实是根本没调用 GPU纯 CPU 硬扛跑了十几个小时还没一个 epoch 完。import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))看到输出里cuda.is_available()为 True 后再检查一下 GPU 显存。如果你是 8GB 以下的卡请严格按后面说的小 batch 配置来。代码里如果有torch.cuda.empty_cache()这种调用不要在训练循环里频繁用它会阻塞异步执行反而变慢。4.2 训练脚本骨架数据加载、模型构造与 loss 观察下面给出一个精简但完整的训练脚本它能跑通整个流程。注意这里刻意省略了验证集逻辑先让训练通起来验证损失留到下一节说。import torch import torchvision from torchvision.models.detection import FasterRCNN from torchvision.models.detection.rpn import AnchorGenerator from torchvision.models.detection.faster_rcnn import FastRCNNPredictor from torch.utils.data import DataLoader, Dataset import xml.etree.ElementTree as ET import os from PIL import Image import numpy as np class MaskDataset(Dataset): def __init__(self, root, transformsNone): self.root root self.transforms transforms self.imgs [f for f in os.listdir(root) if f.endswith(.jpg)] def __getitem__(self, idx): img_path os.path.join(self.root, self.imgs[idx]) xml_path img_path.replace(.jpg, .xml) image Image.open(img_path).convert(RGB) tree ET.parse(xml_path) boxes [] labels [] for obj in tree.findall(object): bnd obj.find(bndbox) xmin float(bnd.find(xmin).text) ymin float(bnd.find(ymin).text) xmax float(bnd.find(xmax).text) ymax float(bnd.find(ymax).text) boxes.append([xmin, ymin, xmax, ymax]) labels.append(1 if obj.find(name).text with_mask else 2) boxes torch.as_tensor(boxes, dtypetorch.float32) labels torch.as_tensor(labels, dtypetorch.int64) target {boxes: boxes, labels: labels} return image, target def __len__(self): return len(self.imgs) def get_model(num_classes3): model torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrainedTrue) in_features model.roi_heads.box_predictor.cls_score.in_features model.roi_heads.box_predictor FastRCNNPredictor(in_features, num_classes) return model if __name__ __main__: dataset MaskDataset(rootdata/mask_dataset) loader DataLoader(dataset, batch_size2, shuffleTrue, collate_fnlambda batch: tuple(zip(*batch))) model get_model() device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) optimizer torch.optim.SGD(model.parameters(), lr0.005, momentum0.9, weight_decay0.0005) lr_scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size5, gamma0.1) for epoch in range(20): model.train() total_loss 0 for images, targets in loader: images [img.to(device) for img in images] targets [{k: v.to(device) for k, v in t.items()} for t in targets] loss_dict model(images, targets) losses sum(loss for loss in loss_dict.values()) optimizer.zero_grad() losses.backward() optimizer.step() total_loss losses.item() lr_scheduler.step() print(fEpoch {epoch}: {total_loss / len(loader)}) torch.save(model.state_dict(), mask_rcnn.pth)这段代码里最关键的参数是 batch_size2因为在 Faster RCNN 的训练中每张图经过 RPN 会生成上千个候选框占用大量显存batch_size 一调大就 OOM。学习率 0.005 只适用于 frozen backbone 的场景如果你解冻了 backbone需要降到 0.0005 左右否则梯度爆炸。SGD 的 momentum 和 weight_decay 是检测任务通用建议不用太动。collate_fn的作用是把多个图片和 target 组装成一个 batch因为每张图的目标数不一样不能直接用默认的堆叠方式。训练结束后你会得到 state_dict但光有这个还不够还要完整保存一个 checkpoint 用于后续推理。4.3 训练过程中 loss 曲线的判读与干预时机训练时盯着终端里的数字没有任何意义你必须把 loss 曲线画出来。最简单的方法是用 TensorBoard在训练代码里加一个SummaryWriter写入 loss。我一般记录四个值rpn_objectness_loss、rpn_box_reg_loss、rcnn_class_loss、rcnn_box_reg_loss。正常情况下前几个 epoch 里 RPN 的 objectness loss 会从 0.6 左右快速掉到 0.3 以下但如果你的数据集小、标注质量差这个值可能一直不下来。这时候不要急着调模型先去看标注框是不是有大量错标、漏标。如果发现 rcnn_class_loss 降得很慢而 RPN loss 降得很快说明 RPN 已经能框到人脸但分类头分不清口罩和裸脸。常见原因是两类样本数量极度不平衡比如 without_mask 只有 100 张with_mask 有 5000 张。解决办法不是在 loss 里加权重而是先做类别重采样让每个 batch 里两类样本大致平衡。我见过有人直接把 class_weight 塞进去然后 loss 变成 nan其实是权重值设置太大导致梯度爆炸。训练中断时不要从头再来。正确做法是每隔 5 个 epoch 保存一次 checkpoint恢复时加载最近的权重并继续训练。这里有个注意点如果换了 batch_size必须重新设置学习率因为 SGD 的噪声尺度跟 batch 大小有关。一般经验是 batch 翻倍学习率翻倍但你显卡撑不住所以更多时候是从大 batch 切到小 batch此时学习率要相应减半。4.4 验证集与 mAP 计算不能只看准确率课程设计里最常见的低级错误是只用“准确率”评价模型。你想想如果测试集里 95% 是没戴口罩的人模型全预测成没戴口罩都有 95% 的准确率这没有任何说服力。必须用 mAPmean Average Precision。torchvision 里提供了torchvision.utils.detection.metrics但功能不全我习惯用 pycocotools 的 COCOeval。如果你的测试标注也是 VOC 格式先转成 COCO 的 JSON然后计算 AP50、AP75 和 mAP。对口罩识别任务我建议重点看 AP50因为人脸检测框不需要像目标检测竞赛那样严格框稍微偏移一点不影响判断是否戴口罩。mAP50 达到 85% 以上对课设来说已经是很不错的成绩。如果 AP50 上不去先检查是不是有大量漏检用下面这段推理脚本把自己测试集的结果可视化看一遍比瞎调参数效率高得多。import torch import torchvision from PIL import Image, ImageDraw model torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrainedFalse) in_features model.roi_heads.box_predictor.cls_score.in_features model.roi_heads.box_predictor torchvision.models.detection.faster_rcnn.FastRCNNPredictor(in_features, 3) model.load_state_dict(torch.load(mask_rcnn.pth, map_locationcpu)) model.eval() img Image.open(test.jpg).convert(RGB) img_tensor torchvision.transforms.functional.to_tensor(img).unsqueeze(0) with torch.no_grad(): pred model(img_tensor)[0] draw ImageDraw.Draw(img) for box, score, label in zip(pred[boxes], pred[scores], pred[labels]): if score 0.5: draw.rectangle(list(box), outlinegreen if label 1 else red, width3) img.save(result.jpg)这段代码输出的结果图能直接告诉你模型是在乱框、漏框还是重复框。如果同一张脸上出现两个高度重叠的框是 NMS 阈值的问题可以把torchvision.ops.nms的 iou_threshold 从默认 0.5 改成 0.4。如果置信度 0.5 以上框太多但很多都是错的说明训练没有收敛或者数据集噪声太大。如果一张脸都没框出来恭喜你回到第 3 章去看你的标注坐标是不是画到图片外面去了。5. 推理部署与演示交互把训练好的模型封装成能展示的完整系统5.1 静态图片、视频流与摄像头实时检测的实现差异训练完模型只是第一步课程设计要求的往往是一个能“看”的系统。静态图片检测最简单直接把上一节的可视化脚本封装成函数就行。视频流检测则要加一个关键逻辑帧采样。如果你每帧都做推理普通 GTX 1660 级别的卡只能跑到 1015 FPS而视频是 30 FPS看起来就会一顿一顿。常见做法是跳帧检测比如每 2 帧检测一次中间那一帧直接用上一帧的结果虽然检测框会有轻微滞后但在口罩检测这种场景下完全够用。摄像头实时检测是答辩时的加分项但也最容易翻车。你需要准备一个能用的 USB 摄像头在笔记本边上放好然后让现场同学当“模特”戴上口罩。这里有一件事必须提前做测试摄像头在 OpenCV 里的打开方式。很多 Windows 笔记本的摄像头被驱动占用cv2.VideoCapture(0)打不开改成cv2.VideoCapture(1)才正常。我建议在系统代码里加一个循环尝试打开 0 到 2 三个索引哪个能打开用哪个并把调试信息打印出来。5.2 封装一个简单的检测服务从脚本到可调用的类做了课设就知道一个 jupyter notebook 里跑通的代码不能叫系统。至少要把检测流程封装成一个类提供detect_image和detect_video两个方法。下面这个类是我常用的骨架它负责模型加载、预处理、推理和结果转成可视化你还可以在此基础上加一个统计功能统计画面里戴与不戴的人数。class MaskDetector: def __init__(self, weights_path, devicecuda, conf_thres0.5): self.device torch.device(device) self.model self._build_model(weights_path) self.conf_thres conf_thres def _build_model(self, weights_path): model torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrainedFalse) in_features model.roi_heads.box_predictor.cls_score.in_features model.roi_heads.box_predictor FastRCNNPredictor(in_features, 3) state torch.load(weights_path, map_locationself.device) model.load_state_dict(state) model.to(self.device) model.eval() return model def detect(self, frame): img_tensor torchvision.transforms.functional.to_tensor(frame).unsqueeze(0).to(self.device) with torch.no_grad(): pred self.model(img_tensor)[0] boxes pred[boxes].cpu().numpy() scores pred[scores].cpu().numpy() labels pred[labels].cpu().numpy() keep scores self.conf_thres return boxes[keep], scores[keep], labels[keep]这个类的好处是隔离了模型细节后面想换模型文件只需要改weights_path想换更低置信度阈值就在初始化时传参数。注意在视频循环里每次调用都要用ascontiguousarray确保输入帧是连续内存否则to_tensor会报奇怪的维度错误。我在这里吃过一次亏OpenCV 读出来的 BGR 帧直接传给模型模型训练时用的是 RGB结果检测结果整个错乱。所以别忘了cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)这是最容易被忽略的坑。5.3 界面选择是做个 GUI 还是用命令行课程设计要求“系统”很多老师期望有一个图形界面。我的建议是分情况如果你们课程重点是算法用一个最简单的 Tkinter 做三按钮界面选图片、开摄像头、退出就足够了如果课程偏软件工程那可以用 PyQt5 做一个完善的界面但结合时间成本不推荐为了界面去学 Qt。Tkinter 里嵌入 OpenCV 的画面需要用PIL.Image.fromarray转成 Tk 能显示的 ImageTk.PhotoImage这个过程会有点卡但答辩演示没问题。另一个思路是做一个本地 Web 服务用 Flask 上传图片、返回检测图。这个方案的优点是你可以在任何设备上访问而且老师可以在手机上投屏看你演示。但注意 Flask 默认的开发服务器是单线程的串行处理请求演示时如果老师连续传图会感觉有点慢。我自己的课设里就是用 Flask 做的因为老师对“网页提交图片”的演示印象深刻比命令行黑窗专业很多。6. 避坑专题Faster RCNN 口罩识别最常见的 5 个翻车现场6.1 显存不足不是显存真的不够是你在 CPU 上做数据加载现象训练刚开始几秒报CUDA out of memory但你明明用的 batch_size 只有 2。原因你不是被模型显存撑爆而是 DataLoader 的num_workers默认是 0所有图片预处理都在主进程每张图都堆在显存里等模型。解决设置num_workers4甚至 8让子进程并行把图片提成张量传来同时把图片 resize 到固定大小比如 800×800不要用原始 4000×3000 的照片训那是在烧显存。我实测一张 800×800 的图只占 200MB 显存一张 4000×3000 的图能占 1.5GB。另一个解决手段是开启 AMP 混合精度。PyTorch 1.10 自带torch.cuda.amp只用改三行代码显存占用直接减半速度还快 20%。但注意混合精度对 loss 的数值稳定性要求更高如果你的 loss 在开启 AMP 后出现 nan先关闭 AMP看看是不是原始 loss 本身就很大不太建议新手在课设里折腾 AMP。6.2 训练 loss 不降反升先怀疑学习率再怀疑数据现象第一个 epoch loss 0.8第二个 epoch 涨到 1.2后面一直在 1.0 附近波动。原因最常见的是学习率太大在 frozen backbone 场景下 0.005 有时就偏高。解决把学习率降到 0.001同时把 SGD 换成 AdamW虽然 Faster RCNN 论文里用的是 SGD但把 backbone 冻结后其实是一个线性分类问题加一个简单回归AdamW 收敛更稳定。我用 AdamW 后 loss 曲线从锯齿状变成了平滑下降。如果降低学习率后仍然不降那就查看你的getitem函数是不是 target 里漏了labels或者 boxes 坐标是int类型导致边界框退化成一个点。还有一个隐蔽问题torch.as_tensor(boxes, dtypetorch.float32)如果 boxes 是嵌套列表有的列表为空也就是某些图片里没有任何标注出现在训练集里的话会直接报错或产生空 target。解决方法是过滤掉没有任何对象的图片或在 Dataset 里跳过。6.3 戴口罩的人脸被识别成没戴口罩分类边界混乱现象眼睛以下都被口罩遮住但模型给的标签还是 without_mask。原因你的训练数据里戴口罩的图片多数是正脸模型学到的是“露出一部分鼻子嘴巴就是没口罩”而侧脸戴口罩时鼻子轮廓不明显模型反而认为是裸脸。解决数据增强加水平和竖直翻转竖直翻转要谨慎使用但水平翻转对侧脸极其有效。同时检查测试图片里的口罩是不是黑色黑色口罩和黑皮肤在低光下边界模糊这时换用色彩抖动增强把亮度和对比度变化范围加大。我自己就遇到过黑色口罩全错的情况后来我把训练集里黑色口罩图片的数量补了 200 张并单独把测试集中的黑色口罩结果挑出来看AP50 硬生生从 78% 涨到 86%。6.4 推理时检测框画不上去或程序卡死坐标类型和图像格式的锅现象用第 4 节的推理脚本画框报错TypeError: argument should be integer or builtin type。原因是pred[boxes]是 float32而ImageDraw.rectangle需要整数坐标。解决box [int(x) for x in box]。如果你把 float 坐标传给 drawPillow 会直接崩。另外一个问题是 OpenCV 画框时用 BGR 格式颜色顺序反过来rectangle颜色参数(0,255,0)画出来是绿色但实际顺序是 BGR如果你要标准绿色是(0,255,0)在 OpenCV 里没问题。代码里最好统一用 RGB 或者统一用 BGR不要混。视频检测卡死通常是因为cv2.imshow之后没有调用cv2.waitKey(1)窗口事件不刷新整个 GUI 线程阻塞。这不算模型问题但确实是演示现场最容易出的意外提前写好if cv2.waitKey(1) 0xFF ord(q): break比现场手忙脚乱好得多。6.5 答辩时老师问“为什么用 Faster RCNN 不用 YOLO”提前准备的三个回答层级这个问题几乎必问。低层级回答Faster RCNN 是两阶段先粗后精精度高。中层级回答口罩检测里小目标多Faster RCNN 的 RPN 比 YOLO 的 anchor 机制更擅长处理遮挡和密集场景。高层级回答两阶段模型在正负样本极度不平衡时有天然的调节机制RPN 输出的候选框让分类头看到的都是高质量样本而 YOLO 的每个位置都要算全类别损失这对少样本的口罩类别不友好。把这三个层级背下来再配合你自己训的 mAP 数字回答就非常扎实。如果你追求极致还可以在答辩现场打开 TensorBoard 展示 RPN 和 RCNN 分开的 loss 曲线老师会认为你真的理解这个系统而不是只会调包。提前把 loss 曲线截图保存到 PPT 最后一页绝对能堵住大部分追问。7. 把模型进一步打磨权值文件分析、阈值调优与跨场景泛化技巧模型训练完、系统跑通课设已经及格了但要拿高分还得再走走。第一个技巧观察置信度阈值对结果的影响。用一组验证图片把置信度从 0.3 到 0.9 每隔 0.1 扫一遍记录每张图的 F1-score。口罩检测这种应用里漏检比误检更严重因为没戴口罩的人被漏检会导致防疫风险所以阈值可以压低到 0.4哪怕误检几个也能接受。你可以写个小脚本自动化这个过程并输出一张曲线图放进报告里。第二个技巧跨场景泛化。你的训练集如果主要是正脸换到俯拍角度比如闸机摄像头大概率会漏检。一个低成本提升泛化能力的方法是用随机仿射变换增强训练集torchvision.transforms.RandomAffine(degrees10, translate(0.1, 0.1), scale(0.8, 1.2))。注意这里变换之后目标框也要做对应的变换如果你用内置的 transforms 不能同时变换目标框需要自定义一个函数。我在自己项目里只加了 degrees±10俯拍测试集上的 AP 提高了大概 4 到 5 个百分点效果非常明显。第三个技巧模型剪枝。课设没必要用完整的 ResNet50 FPN你可以尝试把 backbone 换成 MobileNetV2用 torchvision 里mobilenet_v2做特征提取器再接一个轻量 RPN。这样一来模型大小从 160MB 降到 40MBCPU 推理速度也能提升不少。但要注意换 backbone 之后必须重新训练不能直接加载原来的 ResNet 权重。如果你的课程设计有额外加分项展示一个轻量版本和一个精确版本说明你考虑到了部署监督和实时性权衡这样的答辩深度是完全不同级别的。最后我想说一个自己的习惯每次训练结束不直接删掉模型而是把最后一个 epoch 的权重和最优 epoch 的权重分开保存测试时对比两个版本常常发现最优 epoch 反而不如最后一个稳定。原因可能是最优 epoch 往往对应过拟合的点而最后一个 epoch 经过更多平均泛化更好。所以你保存权重文件时最好每 5 个 epoch 存一次然后选验证集 mAP 最高的那个而不是看训练 loss 最低的。这也是我踩过几次坑之后才养成的习惯。希望这份笔记能把你在课程设计里的摸索时间省出一大截祝你们答辩顺利。本文还有配套的精品资源点击获取