☰
基于Faster-RCNN的遮挡人脸检测实战:从原理到毕业设计
2026/10/1 5:36:22 网站建设 项目流程

简介:本资源为基于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-RCNN(ResNet50-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 python=3.9 -y conda activate frcnn_face # 装 pytorch,注意去官网按你的 CUDA 版本选命令,这里以 cu118 为例 pip install torch==2.0.1 torchvision==0.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.7

pre_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:oy+oh, ox:ox+ow] = 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_classes=2).to(device) # 1 类人脸 + 背景 optimizer = torch.optim.SGD(model.parameters(), lr=0.005, momentum=0.9, weight_decay=0.0005) # 前 500 步 warmup,避免一开始梯度炸掉 lr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.1) dataset = FaceDataset("data/annotations/train.json", "data/images", train=True) loader = DataLoader(dataset, batch_size=4, shuffle=True, collate_fn=lambda 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_norm=10.0) optimizer.step() if step % 50 == 0: print(f"epoch {epoch} step {step} loss {total_loss.item():.4f}") lr_scheduler.step() torch.save(model.state_dict(), f"checkpoints/frcnn_epoch{epoch}.pth")

逻辑说明:batch_size=4是因为 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 和召回。

指标含义遮挡场景关注点
AP@0.5IoU 阈值 0.5 下的平均精度整体水平,答辩必报
Recall@0.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_classes=2) 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 到 1,permute把 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(seed=42): 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 = False

deterministic=True会让 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_factor=s, mode="bilinear")[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、评估这三件事做扎实,剩下的都是锦上添花。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询