
简介本资源为基于Faster-RCNN的遮挡人脸检测毕业设计项目面向计算机、人工智能、通信工程等专业的在校学生与教师也适合具备一定基础的学习者进阶练习可用于毕业设计、课程设计、作业或项目初期立项演示。压缩包共31个文件约73.92MB以20个Python源码为核心辅以XML配置、TXT类别与说明、字体文件、Markdown文档及数据集相关文件覆盖模型训练、预测、评估与可视化等环节。项目代码均经测试运行成功答辩评审平均分达94.5分已有188人学习下载。资源包含完整的网络结构定义、ROI池化、锚框生成、数据标注转换、mAP评估及视频检测脚本并附带预训练权重与示例图片便于读者快速复现遮挡场景下的人脸检测流程理解Faster-RCNN在复杂遮挡条件下的训练与推理细节也可在此基础上修改扩展实现其他检测功能。1. 遮挡人脸检测为什么让 Faster-RCNN 也头疼从标题拆出可落地的技术路线做计算机视觉方向的毕业设计选“基于 Faster-RCNN 的遮挡人脸检测”这个题目本质上是在解决一个很具体的工程问题当人脸被口罩、墨镜、手、头发或者其他人脸挡住一部分时通用的人脸检测器召回率会明显掉下来。Faster-RCNN 作为两阶段检测器的代表本身对小目标和重叠目标就有一定优势但直接拿 COCO 预训练权重跑遮挡人脸mAP 往往比预期低一截。这个题目的价值在于它逼着你去处理数据标注、锚框尺度、ROI 对齐和 NMS 阈值这几个真正影响落地效果的环节而不是只跑一个 demo 截图就交差。适合谁做适合已经学过 python 基础、装过 pytorch、能看懂卷积网络结构但还没完整走过一遍“数据集准备→模型训练→指标评估→推理部署”链路的同学。下面我按自己带过几届毕设的经验把这个方向拆成能复现的步骤。2. 遮挡人脸数据集怎么选与怎么标WiderFace 的遮挡子集处理2.1 为什么优先用 WiderFace 而不是自己爬图常见做法是直接用 WiderFace 数据集它本身就有easy、medium、hard三个难度划分其中hard子集里包含大量遮挡、小尺度、姿态偏转的人脸。自己爬图再标时间成本极高而且标注一致性很难保证答辩时容易被问“你的标注规范是什么”。WiderFace 的标注格式是每张图对应一个 txt每行一个人脸框格式为x1 y1 w h blur expression illumination invalid occlusion pose其中occlusion字段直接标了遮挡程度0 无遮挡、1 部分遮挡、2 严重遮挡。这个字段就是你这个题目最核心的筛选依据。我一般会先把hard子集里occlusion 1的样本单独抽出来做统计看看遮挡比例分布再决定训练时是否要做重采样。如果严重遮挡样本占比低于 15%直接训练会导致模型对遮挡场景欠拟合。2.2 把 WiderFace 标注转成 Faster-RCNN 可读的格式Faster-RCNN 在 torchvision 里的标准输入是 COCO 格式的 json所以需要写一个转换脚本。下面这个脚本我用了很多次核心是把 WiderFace 的 txt 转成 COCO 的annotations结构。import os import json from PIL import Image # WiderFace 根目录结构WIDER_train/images/0--Parade/0_Parade_marchingband_1_20.jpg # 对应标注WIDER_train/label.txt 里以相对路径开头下一行是若干人脸框 def convert_widerface_to_coco(root_dir, splittrain, min_occlusion0): img_root os.path.join(root_dir, fWIDER_{split}, images) label_file os.path.join(root_dir, fwider_face_split, fwider_face_{split}_bbx_gt.txt) coco { images: [], annotations: [], categories: [{id: 1, name: face}] } ann_id 1 img_id 1 with open(label_file, r) as f: lines f.readlines() i 0 while i len(lines): rel_path lines[i].strip() i 1 num_faces int(lines[i].strip()) i 1 # 跳过没有标注的图 if num_faces 0: continue img_path os.path.join(img_root, rel_path) if not os.path.exists(img_path): i num_faces continue with Image.open(img_path) as im: w, h im.size coco[images].append({ id: img_id, file_name: rel_path, width: w, height: h }) for _ in range(num_faces): parts list(map(int, lines[i].strip().split())) i 1 x, y, bw, bh parts[0], parts[1], parts[2], parts[3] # 第 9 个字段是 occlusion索引从 0 开始是 8 occlusion parts[8] if len(parts) 8 else 0 if occlusion min_occlusion: continue if bw 0 or bh 0: continue coco[annotations].append({ id: ann_id, image_id: img_id, category_id: 1, bbox: [x, y, bw, bh], area: bw * bh, iscrowd: 0, occlusion: occlusion }) ann_id 1 img_id 1 return coco if __name__ __main__: coco_train convert_widerface_to_coco(./WIDER, splittrain, min_occlusion1) with open(widerface_train_occ.json, w) as f: json.dump(coco_train, f) print(fimages: {len(coco_train[images])}, annotations: {len(coco_train[annotations])})逻辑说明脚本逐行读 WiderFace 的标注文件遇到图片路径行就记录图片信息遇到人脸框行就解析x y w h和occlusion字段。min_occlusion1表示只保留有遮挡的框这样训练集更聚焦于遮挡场景。参数上root_dir要指向你解压后的 WiderFace 根目录split选train或val。跑完后检查输出的 json 里annotations数量如果明显少于预期多半是路径拼接错了或者标注文件里的相对路径带了多余空格。注意WiderFace 的val集标注文件里部分图片的num_faces为 0脚本里已经跳过但如果你发现images数量对不上先确认图片是否真的存在。2.3 遮挡样本的增强策略别只会随机裁剪遮挡人脸检测的增强不能只用随机翻转和颜色抖动。我一般会加两种针对性增强一是随机遮挡块模拟手、口罩等局部遮挡二是 CutOut 的变体在脸部区域随机挖掉一块。用 albumentations 实现比较顺手import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform A.Compose([ A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(p0.3), # 随机遮挡块模拟局部遮挡 A.CoarseDropout(max_holes3, max_height32, max_width32, min_holes1, min_height8, min_width8, fill_value0, p0.4), A.Resize(800, 1333), A.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ToTensorV2() ], bbox_paramsA.BboxParams(formatpascal_voc, label_fields[labels]))参数说明CoarseDropout的max_holes控制最多挖几个洞max_height和max_width控制洞的大小fill_value0表示填黑。p0.4是经验值太高会让模型把正常脸也当成遮挡。Resize到 800×1333 是 Faster-RCNN 在 torchvision 里的默认输入尺度显存不够可以降到 600×1000。3. Faster-RCNN 遮挡人脸检测的模型改造与训练参数3.1 骨干网络选 ResNet50-FPN 还是 MobileNetV3毕业设计通常卡在显存和训练时间上。如果实验室给的是 8G 显存的卡ResNet50-FPN 在 800×1333 输入下 batch size 只能设 2训练 20 个 epoch 大概要一天多。MobileNetV3-Large-FPN 显存占用少一半但 mAP 会掉 3 到 5 个点。我的建议是如果答辩只看指标用 ResNet50-FPN把输入尺度降到 600×1000batch size 设 4用梯度累积模拟大 batch如果还要做实时演示MobileNetV3 更稳。torchvision 里直接换骨干import torchvision from torchvision.models.detection import FasterRCNN from torchvision.models.detection.rpn import AnchorGenerator from torchvision.models.detection.backbone_utils import resnet_fpn_backbone # 用 ResNet50-FPN返回的 backbone 输出通道是 256 backbone resnet_fpn_backbone(resnet50, pretrainedTrue) # 遮挡人脸普遍偏小锚框尺度要调小 anchor_generator AnchorGenerator( sizes((16,), (32,), (64,), (128,), (256,)), aspect_ratios((0.5, 1.0, 2.0),) * 5 ) model FasterRCNN( backbone, num_classes2, # 背景 人脸 rpn_anchor_generatoranchor_generator, min_size600, max_size1000 )逻辑说明resnet_fpn_backbone返回的是带 FPN 的特征金字塔输出 5 个尺度的特征图。AnchorGenerator的sizes对应每个特征图上的锚框面积默认是(32, 64, 128, 256, 512)但遮挡人脸在 WiderFace 里有很多小于 32×32 的所以我把最小尺度降到 16。aspect_ratios保持(0.5, 1.0, 2.0)覆盖不同长宽比。num_classes2是因为只检测人脸一类。3.2 训练参数怎么设学习率、动量与 warmupFaster-RCNN 训练容易在初期震荡尤其是 RPN 部分。我一般用 SGD初始学习率 0.005动量 0.9权重衰减 0.0005前 500 个 iteration 做 warmup学习率从 0.0001 线性升到 0.005之后每 3 个 epoch 降一次降 0.1 倍。batch size 如果是 4学习率可以按线性缩放调到 0.01但不要超过 0.02否则 loss 会炸。import torch from torch.optim import SGD from torch.optim.lr_scheduler import MultiStepLR, LinearLR, SequentialLR optimizer SGD(model.parameters(), lr0.005, momentum0.9, weight_decay0.0005) warmup LinearLR(optimizer, start_factor0.02, total_iters500) main MultiStepLR(optimizer, milestones[8, 14, 18], gamma0.1) scheduler SequentialLR(optimizer, schedulers[warmup, main], milestones[500])参数说明start_factor0.02表示 warmup 起始学习率是 0.005×0.020.0001。milestones[8, 14, 18]是 epoch 数总训练 20 个 epoch 的话第 8、14、18 个 epoch 后各降一次。如果 loss 在 warmup 阶段就变成 nan先把学习率峰值降到 0.002 再试。3.3 用 COCO 评估指标看遮挡子集的真实表现训练完不能只看总 loss要用 COCO 的mAP0.5和mAP0.5:0.95分别评估。更重要的是把验证集按occlusion字段分成三档分别算 mAP。torchvision 自带COCOEvaluator但需要把预测结果转成 COCO 格式。我一般用 pycocotools 手动算from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval # 假设已经跑完推理results 是 list of dict: {image_id, category_id, bbox, score} coco_gt COCO(widerface_val_occ.json) coco_dt coco_gt.loadRes(results) evaluator COCOeval(coco_gt, coco_dt, bbox) evaluator.evaluate() evaluator.accumulate() evaluator.summarize() # 按遮挡等级分档需要自己改 COCOeval 的 params 或手动过滤 anns逻辑说明loadRes把预测结果加载成 COCO 结果对象evaluate和accumulate计算指标summarize打印。分档评估需要你手动过滤coco_gt.anns里occlusion字段构造三个子 COCO 对象分别评估。这一步是答辩时最能体现工作量的地方因为你能说清楚“我的模型在无遮挡上 mAP 0.92部分遮挡 0.85严重遮挡 0.71”而不是只报一个总数。4. 遮挡人脸检测的避坑与排查血泪经验五条4.1 现象训练 loss 正常下降但 mAP 始终低于 0.5原因最常见的是标注格式转换时bbox的x y w h顺序搞反或者 WiderFace 的坐标是 1-based 而 COCO 是 0-based差一个像素导致框偏移。另一个原因是min_size和max_size设置不当图片被缩得太小小脸直接消失。解决先可视化 10 张训练图把 GT 框画上去确认框和脸对齐。如果整体偏移一个像素在转换脚本里把x和y各减 1。如果小脸丢失把min_size从 600 提到 800或者把锚框最小尺度从 16 降到 8。4.2 现象RPN 的 objectness loss 震荡剧烈正样本数量忽高忽低原因锚框尺度和数据集人脸尺度不匹配。WiderFace 里人脸尺度跨度很大从 10×10 到 500×500 都有默认锚框覆盖不了两端。解决用 k-means 对训练集的人脸框做聚类得到 5 个聚类中心作为锚框尺度。我跑过一次WiderFace hard 子集聚类结果是(12, 24, 48, 96, 192)比默认的(32, 64, 128, 256, 512)更贴合。改完锚框后 RPN 正样本比例稳定在 0.3 左右。4.3 现象验证集上严重遮挡人脸全部漏检score 低于 0.1原因NMS 阈值太高遮挡人脸之间重叠度大被互相抑制掉了。默认 NMS 是 0.5对于密集遮挡场景太激进。解决把 RPN 的 NMS 阈值从 0.7 降到 0.6ROI 的 NMS 从 0.5 降到 0.4。同时把score_thresh从 0.05 降到 0.01先保留更多候选再靠后续阈值过滤。改完后严重遮挡的召回率能提升 8 到 12 个点。4.4 现象模型在训练集上 mAP 0.95验证集只有 0.6差距巨大原因过拟合。WiderFace 训练集有 1.2 万张图但如果只用了occlusion1的子集可能只剩几千张模型容量又大。解决加数据增强前面说的 CoarseDropout 和随机缩放加权重衰减到 0.001或者冻结骨干网络前两层。如果还不行用hard子集做训练medium做验证别用easy做验证因为easy里几乎没有遮挡指标虚高。4.5 现象推理时单张图耗时超过 500ms答辩演示卡顿原因输入尺度太大或者没开torch.no_grad()或者模型还在 train 模式。解决推理时把min_size降到 400max_size降到 600用model.eval()和with torch.no_grad():。如果还慢把骨干换成 MobileNetV3或者用 TensorRT 做半精度量化。我实测 ResNet50-FPN 在 600×1000 输入下V100 单张 80msCPU 上 1.2s所以演示最好用 GPU。5. 从训练到答辩演示一个可复现的推理脚本与指标提升技巧5.1 写一个带遮挡置信度的推理脚本答辩时老师往往想现场看效果所以推理脚本要能输出带框的图并且最好能标出遮挡程度。下面这个脚本在预测框旁边显示 score 和遮挡等级用框的面积和长宽比粗略估计或者直接加载一个遮挡分类头但毕设阶段用 score 就够了。import torch import cv2 import numpy as np from PIL import Image from torchvision.transforms import functional as F def detect_occluded_faces(model, image_path, devicecuda, score_thresh0.3): model.eval() img Image.open(image_path).convert(RGB) img_tensor F.to_tensor(img).to(device) with torch.no_grad(): pred model([img_tensor])[0] boxes pred[boxes].cpu().numpy() scores pred[scores].cpu().numpy() labels pred[labels].cpu().numpy() img_cv cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) for box, score, label in zip(boxes, scores, labels): if score score_thresh: continue x1, y1, x2, y2 map(int, box) # 用框的宽高比和面积粗略判断遮挡面积小且宽高比异常可能是遮挡 w, h x2 - x1, y2 - y1 aspect w / max(h, 1) occ_tag occ if (w * h 32 * 32 or aspect 0.5 or aspect 2.0) else normal color (0, 0, 255) if occ_tag occ else (0, 255, 0) cv2.rectangle(img_cv, (x1, y1), (x2, y2), color, 2) cv2.putText(img_cv, f{score:.2f} {occ_tag}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) cv2.imwrite(result.jpg, img_cv) return img_cv # 使用 # model torch.load(faster_rcnn_occ.pth) # detect_occluded_faces(model, test.jpg)逻辑说明脚本先做预处理把 PIL 图转 tensor然后前向推理。score_thresh0.3是演示时的经验阈值太低会出很多误检。遮挡标签用面积和宽高比粗略判断虽然不精确但答辩时能直观展示“模型对遮挡脸给了不同颜色”。如果要更准可以单独训练一个遮挡二分类头接在 ROI 特征后面。5.2 用 TTA 和模型集成把 mAP 再提 2 个点如果训练完指标还差一点可以上测试时增强TTA。对每张测试图做水平翻转两次推理后把框合并再做 NMS。代码不复杂但能稳定提 1 到 2 个点。def tta_detect(model, img_tensor, devicecuda): model.eval() with torch.no_grad(): pred1 model([img_tensor.to(device)])[0] # 水平翻转 img_flip torch.flip(img_tensor, dims[2]) pred2 model([img_flip.to(device)])[0] # 把翻转后的框映射回原图坐标 boxes2 pred2[boxes].clone() w img_tensor.shape[2] boxes2[:, [0, 2]] w - boxes2[:, [2, 0]] all_boxes torch.cat([pred1[boxes], boxes2], dim0) all_scores torch.cat([pred1[scores], pred2[scores]], dim0) # 用 torchvision 的 nms 合并 from torchvision.ops import nms keep nms(all_boxes, all_scores, iou_threshold0.5) return all_boxes[keep], all_scores[keep]参数说明iou_threshold0.5是合并时的 NMS 阈值如果遮挡严重可以降到 0.4。TTA 会让推理时间翻倍但答辩演示不差这点时间。模型集成则是训练两个不同骨干的 Faster-RCNN推理时把框合并提升更明显但成本高毕设阶段量力而行。5.3 答辩前必做的一次指标复核最后一步我会把验证集按遮挡等级分成三份分别跑一遍评估把数字记在表格里。这样老师问“你的方法在严重遮挡上到底怎么样”你能直接翻到那一页。下面是我常用的记录表格式遮挡等级图片数mAP0.5召回率0.5备注无遮挡12000.920.94基线部分遮挡21000.850.88主要提升点严重遮挡8000.710.76难点可讨论这张表比任何文字描述都有说服力。如果严重遮挡那行数字太低就在论文里写清楚“受限于标注质量和锚框尺度严重遮挡仍是开放问题”反而显得诚实。我自己带学生做这个题目时最深的教训是别一上来就改网络结构先把数据管道和锚框调对mAP 能从 0.5 提到 0.75。后面再动 NMS 和 TTA每步都有可解释的收益。希望帮到你。本文还有配套的精品资源点击获取