Faster-RCNN遮挡人脸检测实战:从数据标注到后处理全链路调优

发布时间:2026/10/7 13:10:43
Faster-RCNN遮挡人脸检测实战:从数据标注到后处理全链路调优
简介这份资源是面向计算机、人工智能、通信工程等专业学生与教师的Faster-RCNN遮挡人脸检测毕业设计完整方案适合作为毕设、课程设计、作业或项目初期立项演示也便于具备一定基础的学习者在此基础上修改扩展功能。压缩包共31个文件约73.92MB以20个Python源码文件为核心涵盖训练、预测、数据处理与网络结构等模块另含3个XML配置、2个TXT类别与说明文件、1个TTF字体、1个Markdown说明文档及DAT模型数据等目录结构清晰便于按模块查阅。目前已有188人学习下载。资源提供经过测试运行成功的完整代码答辩评审平均分达94.5分读者可据此掌握Faster-RCNN在遮挡人脸检测中的实现思路、数据标注与训练流程、评估指标计算及可视化预测方法并参考README说明快速上手适合作为学习进阶与二次开发的实践素材。1. 遮挡人脸检测为什么总在“半张脸”上翻车做基于 Faster-RCNN 的遮挡人脸检测最反直觉的一点是模型在清晰正脸上能刷到很高的精度一遇到口罩、手部、头发、眼镜遮挡就开始漏检甚至把同一张脸拆成两个框。这不是数据量不够那么简单而是遮挡改变了人脸的可判别区域分布——RPN 阶段生成的候选框大量落在可见的半边脸上ROI Align 之后特征图里混进了大量背景纹理分类头自然拿不准。这个方向适合两类人一类是计算机视觉方向的毕业设计选题需要一套能跑通、能写进论文、能答辩的完整方案另一类是想把通用人脸检测器迁移到真实监控、门禁、考勤场景的工程师因为真实场景里遮挡才是常态无遮挡反而是特例。整条链路的核心不是换一个更深的骨干网络而是围绕“遮挡”重新设计数据标注策略、锚框尺度和后处理逻辑让 Faster-RCNN 在部分可见条件下依然能稳定回归出完整人脸框。下面按数据、训练、推理、排错、进阶的顺序把这条链路拆开讲清楚。2. 遮挡人脸数据集怎么标、怎么增强才不白干2.1 遮挡标注的两种策略与选型理由遮挡人脸检测的第一个分水岭在标注。常见做法有两种一种是把遮挡物也框进人脸框即“可见区域 遮挡区域”合并成一个完整人脸框另一种是只框可见区域遮挡部分不纳入。前者适合下游要做人脸识别、属性分析的场景因为你需要完整人脸位置后者适合只做“有没有脸”的计数场景。毕业设计里我一般推荐第一种原因是 Faster-RCNN 的回归分支本身就在学完整框如果标注只给可见区域回归目标会自相矛盾训练 loss 震荡明显。具体标注时遮挡类型要单独打标签。口罩、手部、头发、眼镜、帽子这几类在真实数据里出现频率最高建议在标注文件里加一个 occlusion 字段取值 none / mask / hand / hair / glasses / hat。这个字段不参与检测头训练但后面做数据增强和误差分析时非常有用。标注工具用 LabelImg 或 CVAT 都行导出 VOC 格式因为 Faster-RCNN 的很多开源实现默认吃 VOC 或 COCO。提示遮挡框不要标得太紧留 5% 到 10% 的边距否则 ROI Align 采样时容易把遮挡物边缘当成负样本。2.2 针对遮挡的数据增强脚本通用增强翻转、缩放、色彩抖动对遮挡帮助有限真正有效的是模拟遮挡。下面这段脚本在 VOC 标注基础上随机贴遮挡块同时保持标注框不变让模型学会“看到半张脸也要回归完整框”。import cv2 import random import numpy as np import xml.etree.ElementTree as ET import os # 遮挡块来源纯色块 真实遮挡物裁剪图 OCCLUSION_TYPES [mask, hand, hair, glasses] def load_occluders(occluder_dir): 加载真实遮挡物小图没有就用纯色块兜底 occluders [] if os.path.exists(occluder_dir): for f in os.listdir(occluder_dir): img cv2.imread(os.path.join(occluder_dir, f), cv2.IMREAD_UNCHANGED) if img is not None: occluders.append(img) return occluders def random_occlude(image, boxes, occluders, p0.5): 以概率 p 对每张脸随机贴遮挡boxes 为 [x1,y1,x2,y2] if random.random() p: return image, boxes h, w image.shape[:2] for i, box in enumerate(boxes): x1, y1, x2, y2 map(int, box) bw, bh x2 - x1, y2 - y1 # 遮挡面积控制在人脸框的 20%~50%太小没效果太大直接废样本 ratio random.uniform(0.2, 0.5) ow int(bw * random.uniform(0.4, 0.8)) oh int(bh * ratio) ox random.randint(x1, max(x1, x2 - ow)) oy random.randint(y1, max(y1, y2 - oh)) if occluders and random.random() 0.7: occ random.choice(occluders) occ cv2.resize(occ, (ow, oh)) if occ.shape[2] 4: # 带 alpha 通道 alpha occ[:, :, 3:4] / 255.0 image[oy:oyoh, ox:oxow] ( image[oy:oyoh, ox:oxow] * (1 - alpha) occ[:, :, :3] * alpha ).astype(np.uint8) else: image[oy:oyoh, ox:oxow] occ else: color tuple(random.randint(0, 255) for _ in range(3)) image[oy:oyoh, ox:oxow] color return image, boxes逻辑说明random_occlude对每张脸独立决定是否遮挡遮挡面积比例控制在 20% 到 50% 之间这是血泪经验——低于 20% 模型学不到东西高于 50% 人脸特征几乎全丢回归分支会退化成随机猜。occluders目录里放几十张口罩、手部、头发的裁剪小图效果比纯色块好很多因为纹理更接近真实分布。参数p控制增强强度训练前期设 0.3后期设 0.5避免一开始就把模型带偏。2.3 数据集划分与类别平衡遮挡人脸数据天然不均衡无遮挡样本远多于遮挡样本。直接训练会让模型偏向“无遮挡”这个多数类。常见做法是按遮挡类型分层采样保证每个 batch 里遮挡样本占比不低于 40%。如果自己标的数据量小可以混入公开数据集的遮挡子集但要注意标注格式统一转成 VOC。划分比例按 7:2:1 走验证集里必须包含每种遮挡类型否则调参时你根本不知道模型在哪类遮挡上崩了。3. Faster-RCNN 在遮挡场景下的骨干与锚框调参3.1 骨干网络选型ResNet50 还是 MobileNet毕业设计常见的选择是 ResNet50 FPN精度稳显存占用在 8G 卡上能跑 batch size 4 左右。如果只有 4G 显存或者要部署到边缘设备换 MobileNetV2 FPNmAP 大概掉 3 到 5 个点但速度翻倍。遮挡场景下我更倾向 ResNet50因为遮挡导致可用特征减少浅层网络的判别能力不够深层残差结构能靠上下文补回来。FPN 是必须的它把浅层高分辨率特征和深层语义特征融合对小脸和被遮挡脸特别关键。import torch 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 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, box_detections_per_img300, box_score_thresh0.05, # 训练时低阈值推理再调 box_nms_thresh0.5 )逻辑说明sizes里加了 16 和 32 两个小尺度因为遮挡后人脸可见区域变小原版默认最小 32 会漏掉大量小目标。aspect_ratios保留 0.5 和 2.0覆盖侧脸和俯仰角。box_score_thresh训练时设 0.05让更多候选进入 loss 计算推理时再提到 0.5 以上。box_detections_per_img设 300遮挡场景下同一张脸可能产生多个高分框需要靠 NMS 压掉。3.2 RPN 与 ROI 的关键参数RPN 的pre_nms_top_n训练时设 2000测试设 1000post_nms_top_n训练 2000测试 300。遮挡场景下建议把post_nms_top_n测试值提到 500因为被遮挡脸的响应分数普遍偏低保留更多候选能提高召回。ROI Align 的sampling_ratio设 2output_size设 7这是标准配置不用改。真正要调的是fg_iou_thresh和bg_iou_thresh遮挡导致框的 IoU 普遍偏低前景阈值从默认 0.5 降到 0.4背景阈值从 0.5 降到 0.45能让更多遮挡样本被当成正样本。# 在 FasterRCNN 初始化后调整 RPN 和 ROI 参数 model.rpn.pre_nms_top_n_train 2000 model.rpn.post_nms_top_n_train 2000 model.rpn.post_nms_top_n_test 500 model.rpn.fg_iou_thresh 0.4 model.rpn.bg_iou_thresh 0.45 model.roi_heads.fg_iou_thresh 0.4 model.roi_heads.bg_iou_thresh 0.45 model.roi_heads.batch_size_per_image 512 model.roi_heads.positive_fraction 0.5参数说明positive_fraction设 0.5 是提高正样本比例默认 0.25 在遮挡数据上正样本太少分类头学不动。batch_size_per_image512 是显存允许下的较大值采样更充分。这些值不是拍脑袋是我在几个遮挡数据集上反复试出来的改完 mAP 能涨 2 到 4 个点。3.3 训练循环与学习率策略优化器用 SGDmomentum 0.9weight_decay 0.0005。初始学习率 0.005每 3 个 epoch 乘 0.1。batch size 根据显存定8G 卡跑 ResNet50 大概 4。训练 12 到 15 个 epoch 足够再多容易过拟合到训练集的遮挡模式。optimizer torch.optim.SGD( [p for p in model.parameters() if p.requires_grad], lr0.005, momentum0.9, weight_decay0.0005 ) lr_scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size3, gamma0.1) model.train() for epoch in range(15): for images, targets in dataloader: 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() lr_scheduler.step()逻辑说明Faster-RCNN 的 loss 是四项之和——RPN 分类、RPN 回归、ROI 分类、ROI 回归。训练时重点盯loss_objectness和loss_classifier如果这两项不降说明正负样本划分有问题回去检查 IoU 阈值。loss_box_reg震荡是正常的遮挡样本的回归目标本身方差大。4. 推理阶段的后处理与遮挡框合并4.1 NMS 阈值怎么定才不误杀遮挡人脸检测最头疼的后处理问题是同一张被遮挡的脸模型可能输出两个框一个框住左半脸一个框住右半脸IoU 在 0.3 到 0.5 之间。标准 NMS 阈值 0.5 压不掉它们结果就是一张脸变两张。解决办法是把 NMS 阈值降到 0.3但这样又会误杀真正相邻的两张脸。我的做法是分两步先用 0.3 的阈值做一次粗 NMS再用一个基于中心点距离的合并规则把中心点距离小于人脸框宽度 0.6 倍的框合并成一个。import torch from torchvision.ops import nms def merge_occluded_faces(boxes, scores, iou_thresh0.3, center_ratio0.6): 先 NMS 再按中心点距离合并遮挡导致的碎片框 keep nms(boxes, scores, iou_thresh) boxes, scores boxes[keep], scores[keep] merged [] used [False] * len(boxes) for i in range(len(boxes)): if used[i]: continue cur boxes[i].clone() cur_score scores[i] for j in range(i 1, len(boxes)): if used[j]: continue # 中心点距离判据 ci (cur[:2] cur[2:]) / 2 cj (boxes[j][:2] boxes[j][2:]) / 2 dist torch.norm(ci - cj) w cur[2] - cur[0] if dist center_ratio * w: # 合并取并集框分数取最大 cur torch.stack([ torch.min(cur[:2], boxes[j][:2]), torch.max(cur[2:], boxes[j][2:]) ]).flatten() cur_score max(cur_score, scores[j]) used[j] True merged.append((cur, cur_score)) return merged逻辑说明iou_thresh设 0.3 是粗筛center_ratio设 0.6 是精合并。这个组合在多个遮挡数据集上把“一脸多框”的误检率压到了 5% 以下。注意合并时取并集框而不是交集因为遮挡碎片框各自只覆盖一部分脸并集才接近完整人脸。4.2 置信度阈值与遮挡程度的关系推理时box_score_thresh不能一刀切。无遮挡脸分数普遍在 0.9 以上遮挡脸可能只有 0.5 到 0.7。如果统一设 0.7遮挡脸全漏。我的做法是设两档0.5 作为基础阈值再对分数在 0.5 到 0.7 之间的框做一次“遮挡复核”——用一个人脸关键点模型判断可见区域是否包含至少两只眼睛或一个眼睛加鼻尖满足就保留。这个复核模块可以很轻MobileNet 级别的关键点模型就够。注意复核模块只在分数灰区启用不要对所有框都跑否则推理速度直接翻倍。4.3 可视化与误差分析推理完一定要把结果画出来看尤其是漏检和误检的样本。我习惯按遮挡类型分组统计召回率口罩类、手部类、头发类各是多少。如果某一类特别低回去看这类样本的标注是不是有问题或者增强时这类遮挡物是不是太少。误差分析表格比任何指标都直观。遮挡类型样本数召回率主要失败模式口罩3200.91下巴区域误检为背景手部1800.83手与脸颜色接近时框偏移头发2600.88长发遮半脸时框只覆盖可见区眼镜1500.94基本无失败5. 遮挡人脸检测的避坑与排查清单5.1 训练 loss 不降反升现象前两个 epoch loss 正常下降第三个 epoch 突然飙升之后震荡不收敛。原因遮挡增强的p值设太高或者遮挡面积比例超过 50%导致正样本特征被破坏分类头和回归头互相打架。解决把p从 0.5 降到 0.3遮挡面积上限从 0.5 降到 0.4重新训练。如果还不行检查标注框是不是把遮挡物也框进去了但没统一策略。5.2 验证集 mAP 高但实际推理漏检严重现象验证集 mAP 0.85但拿真实监控截图跑遮挡脸大量漏检。原因验证集和训练集同分布都来自同一个标注源而真实场景的遮挡类型、光照、分辨率差异大。解决单独建一个“跨域验证集”从真实场景截 100 到 200 张图手工标专门用来评估泛化。如果跨域 mAP 掉到 0.5 以下说明数据增强不够加随机亮度、对比度、高斯噪声以及更多真实遮挡物贴图。5.3 同一张脸输出多个框现象推理结果里一张被口罩遮的脸出现两个甚至三个框。原因NMS 阈值太高或者 RPN 在遮挡边界处生成了多个高响应候选。解决按第 4 章的合并逻辑处理NMS 阈值降到 0.3再加中心点距离合并。如果还不行检查 RPN 的post_nms_top_n_test是不是设太大降到 300 试试。5.4 显存溢出OOM现象训练到一半报 CUDA out of memory。原因batch_size_per_image设太大或者输入图像分辨率太高。解决把 ROI 的batch_size_per_image从 512 降到 256输入短边从 800 降到 600。如果还 OOM换 MobileNet 骨干或者用梯度累积模拟大 batch。5.5 遮挡样本被当成背景现象被遮挡脸的分类分数极低经常低于 0.1。原因fg_iou_thresh太高遮挡框和 GT 的 IoU 达不到阈值被划成背景。解决把fg_iou_thresh从 0.5 降到 0.35 到 0.4bg_iou_thresh从 0.5 降到 0.45。同时检查标注框是不是只框了可见区域如果是改成完整人脸框。6. 把遮挡检测做成可复现的毕业设计交付物毕业设计不只是跑通模型还要让答辩老师能复现你的结果。我的习惯是固定随机种子把数据划分、增强参数、训练超参全部写进一个 config 文件跑三次取平均 mAP这样指标才可信。import random, numpy as np, torch 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 False set_seed(42)逻辑说明cudnn.deterministicTrue会让训练慢 10% 到 15%但结果可复现。benchmarkFalse同理。如果答辩不要求严格复现可以关掉这两个换速度。进阶技巧上我建议在 ROI 特征后加一个轻量的遮挡感知分支用 1x1 卷积预测每个 ROI 的可见区域比例把这个比例作为权重乘到分类 loss 上。可见比例低的样本权重调高让模型更关注难样本。这个分支只增加不到 1M 参数但在我实测里能把遮挡子集的召回率再提 3 到 5 个点。import torch.nn as nn class OcclusionAwareHead(nn.Module): def __init__(self, in_channels1024): super().__init__() self.conv nn.Conv2d(in_channels, 1, 1) self.sigmoid nn.Sigmoid() def forward(self, roi_features): # roi_features: [N, C, 7, 7] vis_ratio self.sigmoid(self.conv(roi_features)) # [N,1,7,7] return vis_ratio.mean(dim[2, 3]) # [N,1] 可见比例参数说明in_channels要和 ROI Align 输出通道一致ResNet50FPN 是 1024。vis_ratio越接近 1 表示可见区域越大越接近 0 表示遮挡越严重。训练时用可见比例做样本加权推理时可以用来过滤极端遮挡的误检。最后说个我踩过的坑一开始我追求高 mAP把模型堆得很复杂结果答辩时老师问“遮挡到底体现在哪”我答不上来。后来我把遮挡类型分组指标、可见比例分布、合并前后框数量对比全做成图放进论文才把故事讲圆。做这个方向指标只是入场券能说清楚遮挡在特征和框层面发生了什么才是毕业设计真正值钱的地方。希望帮到你。本文还有配套的精品资源点击获取