
简介本资源为基于Faster-RCNN的遮挡人脸检测毕业设计项目面向计算机、人工智能、通信工程等专业的在校学生与教师也适合具备一定基础的学习者进阶。项目围绕遮挡场景下的人脸检测任务提供从数据标注、模型训练到预测推理的完整流程可作为毕设、课程设计或项目立项演示的参考方案。压缩包共31个文件约73.92MB以20个Python脚本为核心涵盖网络结构、ROI池化、锚框生成与训练预测等模块另含XML标注、TXT类别与说明、字体及模型数据文件目录划分清晰便于按模块阅读与调试。目前已有188人学习下载。代码经测试运行成功答辩评审平均分达94.5分读者可据此掌握Faster-RCNN在遮挡人脸检测中的实现思路并在此基础上修改扩展功能。1. 从一张糊掉的合影说起遮挡人脸检测到底难在哪去年帮一个做校园门禁的团队看现场摄像头装在闸机上方逆光、戴口罩、低头刷手机三种情况叠在一起识别率直接掉到六成。他们原本用的是 MTCNN 那一套人脸框能出来但一到口罩、围巾、手挡脸就漏检。后来换成基于 Faster-RCNN 的遮挡人脸检测方案同一批测试视频里召回率提上去了代价是推理速度慢了一截。这个取舍值不值取决于你的场景是「宁可慢也不能漏」还是「宁可漏也不能卡」。这篇讲的就是怎么用 Faster-RCNN 把遮挡人脸检测这套东西从零跑起来顺带把毕业设计里最容易被答辩老师追问的几个点讲透为什么选两阶段检测器而不是 YOLO 这类单阶段、RPN 的 anchor 怎么按人脸尺寸调、遮挡样本怎么标、训练不收敛时先看哪里。适合正在做计算机毕业设计、需要一份能跑通的 python 源代码和文档说明的同学也适合想把这套方案落到实际项目里的工程师。下面按「先立住原理 → 再动手复现 → 最后避坑」的顺序走每一步都给到能直接抄的命令和参数。2. Faster-RCNN 做遮挡人脸检测为什么两阶段反而更稳2.1 单阶段和两阶段在遮挡场景下的真实差别先说结论遮挡人脸检测这个任务两阶段检测器通常比单阶段更稳原因不在「精度高」这种笼统说法而在候选框的生成方式。单阶段检测器YOLO、SSD 这一类是在特征图上直接回归每个位置的框和类别一个 grid 负责一块区域遮挡导致目标特征被切断时这个 grid 很容易把「半张脸」判成背景。两阶段不一样第一阶段 RPN 只负责「这里像不像有东西」产出大量候选框第二阶段再对每个候选框做 ROI Pooling 后精细分类和回归。遮挡让分类变难但「像不像有东西」这个判断对遮挡没那么敏感所以召回率更稳。代价也很直接RPN 加 ROI Head 两轮计算推理速度比单阶段慢。我实测过同一张 1080P 图YOLOv5s 大概 20ms 出头Faster-RCNNResNet50-FPN 骨干要到 80ms 上下具体看你显卡。所以选型逻辑是如果场景是离线图片审核、门禁抓拍、安防截图分析这类不要求实时 30 帧的Faster-RCNN 值得上如果是车载或高帧率视频流得先想清楚算力够不够。2.2 遮挡人脸检测里 RPN 和 ROI 各自在干什么把 Faster-RCNN 拆开看遮挡人脸检测的流程是这样的骨干网络常见是 ResNet50 FPN提特征FPN 把浅层高分辨率特征和深层语义特征融合这对小脸和被遮挡的脸很关键因为浅层保留了边缘和纹理。RPN 在特征图每个位置铺一组 anchor判断前景/背景并回归偏移。人脸是近似 1:1 到 1:1.5 的竖长条anchor 比例要往这个方向调别用 COCO 默认那套。ROI Align比 ROI Pooling 更准避免量化误差把候选框对应的特征抠出来统一到 7x7。分类头输出「人脸 / 背景」回归头再精修框位置。遮挡主要影响的是分类头因为被挡住的部分特征缺失。缓解手段有两个方向一是数据层面把遮挡样本喂够二是结构层面用 FPN 多尺度特征让没被挡的那部分特征也能支撑判断。这两点后面都会落到具体操作。2.3 环境准备python 版本、依赖和目录结构动手前先把环境钉死毕业设计最怕的就是「在我电脑上能跑」。我一般用 python 3.8 或 3.9太新的版本有些检测库轮子还没跟上。下面这套是能跑通的最小依赖# 建议用 conda 建独立环境避免和系统 python 打架 conda create -n frcnn_face python3.9 -y conda activate frcnn_face # 装 pytorch注意去官网按你的 CUDA 版本选命令这里以 cu118 为例 pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cu118 # 检测框架和常用工具 pip install opencv-python pillow numpy matplotlib tqdm pyyaml tensorboard逻辑说明torch 和 torchvision 版本必须配套2.0.1 配 0.15.2 是验证过的组合乱配会出现torchvision::nms找不到符号这种玄学报错。CUDA 版本按你显卡驱动来不确定就先跑nvidia-smi看右上角的 CUDA Version。参数上--index-url指向官方轮子源比默认源快很多。目录结构建议这样组织答辩时老师一看就知道你工程习惯frcnn_face/ ├── data/ │ ├── images/ # 原图 │ └── annotations/ # 标注文件 ├── configs/ # 训练配置 yaml ├── models/ # 骨干网络和检测头定义 ├── utils/ # 数据加载、可视化、评估 ├── train.py ├── predict.py └── requirements.txt提示requirements.txt 里把每个包的版本号写死答辩机器上pip install -r requirements.txt一把过能省掉大量现场调试时间。3. 数据这一关遮挡人脸标注和 anchor 尺寸怎么定3.1 遮挡人脸的标注规则什么算脸什么算背景数据决定上限这句话在遮挡人脸检测里尤其成立。标注时最容易吵起来的是只露半张脸算不算戴口罩算不算我的规则是——只要人眼能判断「这是一个人脸区域」就标框住可见部分加合理外推。具体三条遮挡面积小于 30%正常标全脸框框包含被遮挡区域。遮挡面积 30% 到 70%仍然标但在标注文件里加一个occlusion属性方便后续做难度分析。遮挡超过 70%只剩一条边或一只眼建议单独归为「困难样本」训练时可以给更高权重或者干脆不标避免污染。标注格式用 VOC 的 XML 或直接转成 COCO json 都行。毕业设计里我推荐 COCO 格式因为主流检测框架都吃这个评估指标也现成。下面是把 VOC 转 COCO 的核心逻辑import xml.etree.ElementTree as ET import json, os def voc_to_coco(xml_dir, out_json): coco {images: [], annotations: [], categories: [{id: 1, name: face}]} ann_id 1 for idx, xml_file in enumerate(os.listdir(xml_dir)): tree ET.parse(os.path.join(xml_dir, xml_file)) root tree.getroot() # 图片宽高从 size 节点读别写死 w int(root.find(size/width).text) h int(root.find(size/height).text) coco[images].append({id: idx, file_name: xml_file.replace(.xml, .jpg), width: w, height: h}) for obj in root.findall(object): bbox obj.find(bndbox) x1 float(bbox.find(xmin).text) y1 float(bbox.find(ymin).text) x2 float(bbox.find(xmax).text) y2 float(bbox.find(ymax).text) # COCO 的 bbox 是 [x, y, w, h]不是角点这里最容易翻车 coco[annotations].append({ id: ann_id, image_id: idx, category_id: 1, bbox: [x1, y1, x2 - x1, y2 - y1], area: (x2 - x1) * (y2 - y1), iscrowd: 0}) ann_id 1 json.dump(coco, open(out_json, w))逻辑说明COCO 的 bbox 是左上角坐标加宽高VOC 是左上右下两个角点转换时x2-x1、y2-y1这一步写错训练时框会整体偏移loss 死活降不下去。参数上category_id从 1 开始0 在 COCO 里是背景保留位。iscrowd标 0 表示单个人脸如果是一群人挤在一起的大框才标 1。3.2 anchor 尺寸按人脸分布来别照搬 COCO 默认值RPN 的 anchor 是检测器的「先验」设错了后面怎么调都别扭。COCO 默认 anchor 是 8/16/32 三种尺度乘 0.5/1/2 三种比例那是给通用目标用的。人脸是竖长条比例集中在 1:1 到 1:1.5尺寸上如果做门禁抓拍人脸框边长大概 40 到 200 像素。我的做法是先统计训练集里所有人脸框的宽高画个分布图然后取 25%、50%、75% 分位数当 anchor 尺度。代码很简单import numpy as np # boxes 是 N x 4 的 [x1,y1,x2,y2] sizes np.sqrt((boxes[:,2]-boxes[:,0]) * (boxes[:,3]-boxes[:,1])) print(25%:, np.percentile(sizes, 25)) print(50%:, np.percentile(sizes, 50)) print(75%:, np.percentile(sizes, 75))假设统计出来是 32、64、128那 anchor 尺度就设[32, 64, 128]比例设[0.8, 1.0, 1.25]。这样 RPN 生成的候选框和人脸真实尺寸更贴合正样本比例上得去训练收敛快。参数改在配置文件里一般长这样rpn: anchor_scales: [32, 64, 128] anchor_ratios: [0.8, 1.0, 1.25] anchor_stride: 16 pre_nms_top_n: 2000 # 训练时每张图保留的候选框数 post_nms_top_n: 1000 # NMS 之后送 ROI 的数量 nms_thresh: 0.7pre_nms_top_n和post_nms_top_n调大能提召回但显存和耗时跟着涨。遮挡场景我一般把post_nms_top_n从默认 300 提到 1000因为被遮挡的脸得分低容易被 NMS 提前筛掉多留一些给第二阶段判断。3.3 数据增强遮挡样本怎么造出来真实遮挡样本往往不够得靠增强补。除了常规的随机翻转、亮度抖动针对遮挡我加两类随机贴块在脸上随机位置贴一个纯色或纹理块模拟口罩、手、围巾。块面积控制在人脸框的 10% 到 40%。Cutout 变体直接抠掉一块填 0逼模型学会用剩余特征判断。import random, cv2 def random_occlude(img, box, ratio_range(0.1, 0.4)): x1, y1, x2, y2 map(int, box) fw, fh x2 - x1, y2 - y1 ratio random.uniform(*ratio_range) ow, oh int(fw * ratio), int(fh * ratio) ox random.randint(x1, max(x1, x2 - ow)) oy random.randint(y1, max(y1, y2 - oh)) # 用均值色块比纯黑更接近真实遮挡物 img[oy:oyoh, ox:oxow] img.mean(axis(0, 1)) return img逻辑说明贴块位置随机但必须落在人脸框内否则等于没遮挡。用图像均值色而不是纯黑是因为纯黑块会让模型学到「黑色遮挡」这种捷径换到真实场景就失效。参数ratio_range上限别超过 0.5遮太多连人都认不出标成负样本反而有害。4. 训练、评估和推理把模型真正跑起来4.1 训练脚本的关键参数和 loss 曲线怎么看训练主循环里Faster-RCNN 的 loss 由四部分组成RPN 的分类和回归、ROI 的分类和回归。看 loss 曲线时分开看别只盯 total。常见现象是 RPN 的 objectness loss 很快降到很低但 ROI 的分类 loss 卡住不降这通常意味着候选框质量不行或者正负样本比例失衡。import torch from torch.utils.data import DataLoader from models.frcnn import build_model from utils.dataset import FaceDataset device torch.device(cuda if torch.cuda.is_available() else cpu) model build_model(num_classes2).to(device) # 1 类人脸 背景 optimizer torch.optim.SGD(model.parameters(), lr0.005, momentum0.9, weight_decay0.0005) # 前 500 步 warmup避免一开始梯度炸掉 lr_scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size3, gamma0.1) dataset FaceDataset(data/annotations/train.json, data/images, trainTrue) loader DataLoader(dataset, batch_size4, shuffleTrue, collate_fnlambda x: tuple(zip(*x))) for epoch in range(12): model.train() for step, (imgs, targets) in enumerate(loader): imgs [img.to(device) for img in imgs] targets [{k: v.to(device) for k, v in t.items()} for t in targets] loss_dict model(imgs, targets) total_loss sum(loss for loss in loss_dict.values()) optimizer.zero_grad() total_loss.backward() # 梯度裁剪遮挡样本梯度波动大时很有用 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm10.0) optimizer.step() if step % 50 0: print(fepoch {epoch} step {step} loss {total_loss.item():.4f}) lr_scheduler.step() torch.save(model.state_dict(), fcheckpoints/frcnn_epoch{epoch}.pth)逻辑说明batch_size4是因为 Faster-RCNN 显存吃得多1080Ti 上 4 张 1080P 图差不多到顶显存小就降到 2。学习率 0.005 配 SGD 是检测任务的常用起点太大 loss 会震荡太小收敛慢。clip_grad_norm_这个梯度裁剪在遮挡场景很有用因为困难样本的梯度容易突然变大不裁的话 loss 会突然飙一个尖峰然后崩掉。StepLR每 3 个 epoch 降一次学习率配合 12 个 epoch 的总量是毕业设计这种数据规模比较稳的配置。4.2 评估指标mAP 之外还要看遮挡子集的召回光看整体 mAP 会骗人。遮挡人脸检测必须按遮挡程度分桶评估否则模型可能只是把简单样本学好了一遇遮挡就漏。做法是在验证集里按occlusion属性分成「无遮挡 / 轻度 / 重度」三组分别算 AP 和召回。指标含义遮挡场景关注点AP0.5IoU 阈值 0.5 下的平均精度整体水平答辩必报Recall0.5召回率遮挡场景比精度更重要重度遮挡子集 AP遮挡50% 样本的 AP直接反映模型抗遮挡能力误检率 FPPI每张图误检数门禁场景误检比漏检更烦评估脚本核心是遍历验证集对每张图跑推理再和 GT 按 IoU 匹配。这里有个坑NMS 阈值设太高重叠的人脸会被合并设太低同一张脸出多个框。人脸场景我一般用 0.4 到 0.5比通用检测的 0.5 略低因为人脸框重叠本来就少。4.3 推理和可视化一张图跑通检测推理阶段和训练的区别是要加torch.no_grad()并且把模型切到 eval 模式否则 BN 层和 dropout 会用训练时的统计量结果不稳定。import torch, cv2 from models.frcnn import build_model model build_model(num_classes2) model.load_state_dict(torch.load(checkpoints/frcnn_epoch11.pth)) model.eval().cuda() img cv2.imread(test.jpg) rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) tensor torch.from_numpy(rgb).permute(2, 0, 1).float().div(255).cuda() with torch.no_grad(): pred model([tensor])[0] # 只保留置信度大于 0.6 的框 for box, score in zip(pred[boxes], pred[scores]): if score 0.6: continue x1, y1, x2, y2 map(int, box.tolist()) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, f{score:.2f}, (x1, y1-5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(result.jpg, img)逻辑说明输入归一化到 0 到 1permute把 HWC 转成 CHW这是 pytorch 的标准输入格式。置信度阈值 0.6 是遮挡场景的经验值比通用检测的 0.5 略高因为遮挡容易产生低分误检宁可漏一点也别让误检框满屏跑。如果场景要求高召回可以降到 0.4但要做好后处理过滤。5. 避坑指南训练不收敛、框偏移、显存爆炸的排查5.1 现象loss 一直不降RPN 输出全是背景原因通常是 anchor 尺寸和人脸尺寸严重不匹配导致 RPN 里几乎没有正样本。IoU 匹配时如果 anchor 和任何 GT 的 IoU 都低于 0.3全被判成负样本RPN 学不到东西。解决先跑一遍 anchor 统计确认尺度覆盖了人脸尺寸分布。临时验证可以把anchor_scales设成[16, 32, 64, 128, 256]这种大范围看 loss 是否开始降降了再往回收。另外检查标注文件里 bbox 坐标是不是归一化过的有些标注工具默认输出 0 到 1 的归一化坐标直接喂进去框全在左上角一小块。5.2 现象训练 loss 正常但推理框整体偏移血泪经验十有八九是 bbox 回归的编码解码不一致。训练时 GT 编码用了某种形式比如中心点加宽高的 log 形式推理解码时必须用完全对应的逆变换。中间任何一步宽高顺序写反框就会偏。解决把编码和解码函数单独拎出来做单元测试拿一个已知框编码再解码看能不能还原。还原不了就是这里的问题。另外检查 ROI Align 的spatial_scale参数它要和特征图相对原图的缩放比例一致FPN 多尺度下每个 level 的 scale 不同写错也会导致框偏移。5.3 现象训练到一半显存爆炸 OOM原因一般是post_nms_top_n设太大或者 batch 里混进了超大图。Faster-RCNN 第二阶段要对每个候选框做 ROI 操作候选框数量直接决定显存占用。解决先把post_nms_top_n从 1000 降到 300 试试能跑再往上加。数据加载时统一 resize 到固定短边比如 800长边不超过 1333这是检测任务的标准做法。如果还爆把 batch_size 降到 1配合梯度累积模拟大 batch。5.4 现象验证集 mAP 还行但遮挡样本全漏原因通常是训练集里遮挡样本太少模型没见过。或者数据增强的遮挡块太规律模型学到了捷径。解决统计训练集里遮挡样本占比低于 20% 就补。增强时遮挡块的位置、大小、颜色都要随机化别用固定模板。另外可以在 loss 里给困难样本加权或者用 focal loss 的思路缓解正负样本失衡。评估时一定分桶看别被整体 mAP 蒙蔽。5.5 现象换机器跑结果对不上原因多半是随机种子没固定或者 cuDNN 的 benchmark 模式导致卷积算法不确定。解决训练脚本开头固定所有随机源import torch, numpy as np, random def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark FalsedeterministicTrue会让 cuDNN 只用确定性算法速度略慢但结果可复现。答辩演示前一定固定种子重跑一遍避免现场结果和文档对不上。6. 把遮挡人脸检测做扎实的两个进阶技巧第一个技巧是难例挖掘。训练几轮后把验证集里漏检和误检的样本挑出来人工确认标注没问题后加进训练集再微调。这个循环做两三轮重度遮挡子集的召回通常能提几个点。代码上就是跑推理、和 GT 比对、把 IoU 低于阈值但置信度高的框对应的图存下来。注意别把标注错误的样本加进去否则越训越歪。第二个技巧是测试时增强TTA。推理时把图水平翻转、多尺度缩放各跑一遍把结果做 NMS 融合。遮挡场景下翻转后原本被挡的那侧可能露出来融合能补回一些漏检。代价是推理时间翻几倍适合离线审核场景。实现上就是维护一个结果列表所有变体的框汇总后统一做一次 NMS。def tta_predict(model, img_tensor, scales(0.8, 1.0, 1.2)): all_boxes, all_scores [], [] for s in scales: scaled torch.nn.functional.interpolate( img_tensor[None], scale_factors, modebilinear)[0] with torch.no_grad(): pred model([scaled])[0] # 框坐标要还原回原图尺度 all_boxes.append(pred[boxes] / s) all_scores.append(pred[scores]) boxes torch.cat(all_boxes) scores torch.cat(all_scores) keep torchvision.ops.nms(boxes, scores, 0.5) return boxes[keep], scores[keep]逻辑说明每个尺度推理完框坐标要除以缩放系数还原到原图坐标系这一步漏了框会整体缩放错位。最后统一 NMS阈值 0.5 是融合时的常用值比单次推理的阈值略高避免同一张脸留下太多重复框。TTA 不是万能的如果模型本身对遮挡就没学好融合也救不回来所以先保证基础训练扎实再上 TTA。我自己做这类项目的习惯是先把 baseline 跑通、指标记下来再一个一个加改进每加一个就对比一次指标涨了留、没涨删。毕业设计最忌讳堆一堆花哨模块但说不清每个的作用。这套 Faster-RCNN 遮挡人脸检测的方案核心就是把数据、anchor、评估这三件事做扎实剩下的都是锦上添花。希望帮到你。本文还有配套的精品资源点击获取