ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Faster-RCNN的遮挡人脸检测:从数据工程到RoI特征重加权实战

基于Faster-RCNN的遮挡人脸检测:从数据工程到RoI特征重加权实战 简介本资源为基于Faster-RCNN的遮挡人脸检测毕业设计项目面向计算机、人工智能、通信工程等专业的在校学生及教师适合作为毕业设计、课程设计或项目立项演示的完整参考方案。压缩包共31个文件约73.92MB以20个Python源码文件为核心辅以XML配置、TXT类别说明、字体文件及Markdown说明文档涵盖数据标注、模型训练、预测推理与评估等完整流程。项目代码均经测试运行成功答辩评审平均分达94.5分已有188人学习下载。读者可获得从VOC数据集处理、锚框生成、ROI池化到ResNet骨干网络搭建的完整实现并附有遮挡人脸检测的预测脚本与可视化工具便于理解Faster-RCNN在复杂场景下的应用逻辑也可在此基础上修改扩展实现其他检测功能。1. 遮挡人脸检测为什么让 Faster-RCNN 也头疼从毕业设计选题到能跑通的方案做计算机视觉方向的毕业设计选“基于 Faster-RCNN 的遮挡人脸检测”这个题目的人不少但真正把 mAP 跑上去、把文档写扎实的并不多。原因很直接人脸检测本身已经够成熟了WIDER FACE 上简单子集早就刷到 0.95 以上可一旦加上“遮挡”这个约束口罩、手、头发、眼镜、围巾这些遮挡物会让人脸的有效特征面积骤减RPN 生成的候选框大量落在遮挡物上分类头拿到的特征图里混着大量非人脸纹理模型就开始“玄学”了——同一张图换个 IoU 阈值AP 能差十几个点。这个选题的价值恰恰在这里它不是让你复现一个已经饱和的 benchmark而是逼你处理一个真实场景里高频出现、但公开方案讲得不够细的问题。适合谁做适合已经学过 PyTorch 基础、跑过至少一个检测 demo、想在毕业设计里体现“调参 数据工程 消融实验”完整链条的本科生。你不需要从零写 backbone但需要理解 Faster-RCNN 的两阶段结构为什么在遮挡场景下会失效以及怎么用数据增强、anchor 调整、损失函数改造把它拉回来。我见过太多这个题目的翻车现场有人直接拿 torchvision 的预训练权重在自建小数据集上 fine-tune结果验证集 loss 震荡不收敛有人把 WIDER FACE 的标注直接转成 VOC 格式就开训忽略了遮挡标注的invalid和occlusion字段还有人文档里只贴了训练命令没写清楚数据划分和评估协议答辩时被问“你的 mAP 是在哪个子集上算的”直接卡住。这篇笔记就按“理论先立住、再动手能复现”的顺序把这条链路拆开讲清楚让你从选题到跑通到写文档都有据可依。2. Faster-RCNN 在遮挡人脸上的结构适配从 backbone 到 RPN 的选型理由2.1 为什么遮挡场景下 backbone 的浅层特征比深层更重要Faster-RCNN 的标准结构是 backbone RPN RoIHead。在通用目标检测里大家习惯用 ResNet-50 的最后一层 C5 做特征因为语义信息强。但遮挡人脸不一样当人脸被口罩遮住下半部分眼睛和眉骨这些判别性区域还在它们对应的是 C3、C4 级别的中层特征感受野小、空间分辨率高。如果你只用 C5经过多次下采样后眼睛区域可能只剩几个像素特征早就糊了。常见做法是用 FPNFeature Pyramid Network把 C3、C4、C5 融合起来让 RPN 在不同尺度上都能拿到有效响应。我一般会保留 P2 到 P5 四个层级其中 P2 负责小脸和局部遮挡P5 负责大脸整体。这里有个参数要盯住FPN 的输出通道数通常设 256如果你显存吃紧可以降到 128但不要再低了否则 RoIAlign 出来的特征维度不够分类头容易欠拟合。另一个选型点是 backbone 用 ResNet-50 还是 MobileNetV2。毕业设计的算力通常有限如果你只有一张 6G 显存的卡ResNet-50 FPN 在 512×512 输入下 batch size 只能开到 2训练会非常慢。这时候换 MobileNetV2 做 backbone参数量从 25M 降到 3.4Mbatch size 能开到 8收敛速度反而更快。代价是 mAP 会掉 2 到 3 个点但毕业设计里“能跑通 有消融对比”比“刷到 SOTA”更实际。2.2 RPN 的 anchor 尺寸怎么根据遮挡人脸分布来调RPN 默认的 anchor 是 8、16、32 三种尺度对应 128²、256²、512² 的框。这个设置在 COCO 上没问题但人脸数据集的尺度分布完全不同。WIDER FACE 里超过 60% 的人脸框短边小于 50 像素如果你直接用默认 anchor大量小脸在 RPN 阶段就被滤掉了召回率上不去。我的做法是先统计训练集的人脸框尺寸分布用 k-means 聚出 5 到 6 个聚类中心然后把 anchor 尺度改成聚类结果。具体操作是读标注文件提取所有bbox的宽高做归一化后跑 k-means。代码不复杂但这一步能直接把 RPN 的召回率从 0.72 拉到 0.85 以上。import numpy as np from sklearn.cluster import KMeans # 读取所有训练集标注提取宽高 widths, heights [], [] for ann in train_annotations: for obj in ann[objects]: w obj[bbox][2] - obj[bbox][0] h obj[bbox][3] - obj[bbox][1] widths.append(w) heights.append(h) # 归一化后聚类k6 sizes np.array(list(zip(widths, heights))) sizes_norm sizes / sizes.max(axis0) kmeans KMeans(n_clusters6, random_state42).fit(sizes_norm) anchors kmeans.cluster_centers_ * sizes.max(axis0) print(聚类得到的 anchor 尺寸) for a in anchors: print(f width{a[0]:.1f}, height{a[1]:.1f})这段代码的逻辑是先把所有框的宽高归一化到同一量纲避免大框主导聚类然后用 k-means 聚成 6 类对应 RPN 的 6 个 anchor最后反归一化回原始像素尺度。参数上n_clusters设 6 是因为 RPN 每个位置默认 3 个尺度 × 3 个长宽比 9 个 anchor你可以把长宽比固定为 1:1、1:2、2:1尺度用聚类结果替换这样总数还是 9不用改 RPN 的输出通道。跑完这一步把得到的 anchor 尺寸写进 config 文件重新训练 RPN观察rpn_objectness_loss是否下降更快。2.3 RoIAlign 的采样点数对遮挡区域的影响RoIAlign 是从特征图上为每个候选框提取固定尺寸特征通常是 7×7。采样点数sampling_ratio默认是 2意思是每个输出格点用 2×2 个采样点做双线性插值。在遮挡场景下如果采样点太少遮挡物边缘的像素会被平均掉导致特征不具判别性。我一般会把sampling_ratio从 2 提到 4输出尺寸保持 7×7。这样每个 RoI 的特征提取更细代价是 RoIHead 的计算量增加约 30%。如果你的 GPU 允许这个改动对遮挡人脸的 AP 提升在 1.5 到 2 个点左右。注意不要提到 8收益递减明显而且显存占用会翻倍。提示改完 anchor 和 sampling_ratio 后先冻结 backbone 只训 RPN 和 RoIHead 各 2 个 epoch确认 loss 正常下降再解冻全量训练否则容易一开始就震荡。3. 数据工程遮挡人脸标注的清洗、增强与格式转换3.1 WIDER FACE 的遮挡标注怎么读、怎么用WIDER FACE 的标注文件是 MAT 格式里面每个框有blur、expression、illumination、occlusion、pose、invalid六个属性。其中occlusion分三档0 表示无遮挡1 表示部分遮挡2 表示严重遮挡。invalid为 1 的框表示这个框无效训练时必须跳过。很多人直接拿wider_face_train_bbx_gt.txt转 VOC把invalid框也转进去了结果训练集里混着大量标注错误的样本模型学出来的框位置偏移严重。正确做法是在转换脚本里加过滤条件invalid 0且occlusion 2。如果你专门做遮挡检测可以把occlusion 1的框单独统计看看占比通常能到 40% 以上这部分就是你的核心难例。import scipy.io as sio # 读取 WIDER FACE 的 MAT 标注 ann sio.loadmat(wider_face_train.mat) event_list ann[event_list][0] file_list ann[file_list][0] face_bbx_list ann[face_bbx_list][0] valid_boxes [] for i, event in enumerate(event_list): event_name event[0] for j, fname in enumerate(file_list[i][0]): img_name fname[0] bboxes face_bbx_list[i][0][j][0] # 过滤 invalid 和 occlusion 属性 for bbox in bboxes: x, y, w, h bbox[:4] invalid bbox[4] if len(bbox) 4 else 0 occlusion bbox[7] if len(bbox) 7 else 0 if invalid 0: valid_boxes.append({ image: f{event_name}/{img_name}, bbox: [x, y, x w, y h], occlusion: occlusion }) print(f有效框数量{len(valid_boxes)})这段代码的关键是bbox数组的索引前 4 位是 x、y、w、h第 5 位是 blur第 6 位是 expression第 7 位是 illumination第 8 位是 occlusion第 9 位是 pose第 10 位是 invalid。不同版本的 WIDER FACE 标注顺序可能略有差异跑之前先打印一个bbox看看长度和值。过滤后得到的valid_boxes再按 8:1:1 划分训练、验证、测试集注意同一 event 的图片要分到同一个集合避免数据泄露。3.2 针对遮挡的增强策略Cutout、GridMask 和随机擦除怎么选通用增强里随机翻转、颜色抖动对遮挡检测帮助有限真正有效的是模拟遮挡的增强。常见的有三种Cutout 是随机挖一个固定大小的方块置零GridMask 是按网格规律挖掉多个小方块随机擦除Random Erasing是随机选一个矩形区域用随机值填充。我一般用 GridMask 随机擦除组合。GridMask 的参数d控制网格单元大小ratio控制挖掉的比例我设d64, ratio0.4这样在 512×512 输入下每张图大约有 40% 的区域被规律性遮挡模拟口罩、围巾这类连续遮挡。随机擦除的概率设 0.5擦除面积占比 0.02 到 0.2模拟手、手机等随机遮挡物。注意不要同时用 Cutout 和 GridMask两者叠加会让有效像素过少模型欠拟合。另外增强只加在训练集验证集和测试集保持原图否则评估指标不可比。3.3 从 WIDER FACE 到 VOC 格式转换脚本与四个边界坑Faster-RCNN 的训练代码通常吃 VOC 格式的 XML 标注。转换脚本本身不长但有几个边界情况容易翻车。第一个坑是坐标越界。WIDER FACE 里有些框的 x w 超过了图片宽度直接写进 XML 会导致训练时 RoIAlign 采样越界报错。解决方法是转换时做 clipx2 min(x w, img_width - 1)y2 min(y h, img_height - 1)。第二个坑是宽高为 0 的框。有些标注里 w 或 h 是 0这种框在计算 IoU 时会出 NaN。转换时加判断if w 1 or h 1: continue。第三个坑是文件名冲突。WIDER FACE 的图片按 event 分文件夹不同 event 下可能有同名文件。转换时要把文件名改成event_name _ original_name否则复制图片时会覆盖。第四个坑是 XML 里的difficult字段。VOC 格式里difficult1的框在评估时会被忽略但训练时仍然参与 loss 计算。如果你想把严重遮挡的框标记为 difficult需要确认你的评估脚本是否支持这个字段否则 mAP 会虚高。import os import xml.etree.ElementTree as ET from PIL import Image def convert_to_voc(valid_boxes, img_dir, out_dir): os.makedirs(out_dir, exist_okTrue) for item in valid_boxes: img_path os.path.join(img_dir, item[image]) img Image.open(img_path) w_img, h_img img.size x1, y1, x2, y2 item[bbox] # 边界裁剪 x1, y1 max(0, x1), max(0, y1) x2, y2 min(w_img - 1, x2), min(h_img - 1, y2) if x2 - x1 1 or y2 - y1 1: continue # 构建 XML annotation ET.Element(annotation) ET.SubElement(annotation, filename).text item[image].replace(/, _) size ET.SubElement(annotation, size) ET.SubElement(size, width).text str(w_img) ET.SubElement(size, height).text str(h_img) obj ET.SubElement(annotation, object) ET.SubElement(obj, name).text face ET.SubElement(obj, difficult).text 1 if item[occlusion] 2 else 0 bndbox ET.SubElement(obj, bndbox) ET.SubElement(bndbox, xmin).text str(int(x1)) ET.SubElement(bndbox, ymin).text str(int(y1)) ET.SubElement(bndbox, xmax).text str(int(x2)) ET.SubElement(bndbox, ymax).text str(int(y2)) tree ET.ElementTree(annotation) xml_name item[image].replace(/, _).replace(.jpg, .xml) tree.write(os.path.join(out_dir, xml_name))这段脚本跑完后检查生成的 XML 数量是否和有效框数量一致再随机抽 5 个 XML 打开看坐标是否在图片范围内。确认无误后再开始训练否则训练到一半报越界错误浪费的是你自己的时间。4. 训练配置与调参学习率、损失权重和评估协议4.1 学习率 warmup 和衰减策略在遮挡数据集上的设置Faster-RCNN 的训练对学习率很敏感。如果你用 ResNet-50 预训练权重初始学习率设 0.001 到 0.002 比较稳如果用 MobileNetV2可以设到 0.004。但直接上大学习率前几个 epoch 的 loss 会炸所以需要 warmup。我一般用线性 warmup前 500 个 iteration 从 0.0001 线性升到初始学习率之后用 cosine 衰减到 0.00001。这个策略在遮挡数据集上比 step 衰减更平滑因为遮挡样本的 loss 波动大step 衰减容易在衰减点附近震荡。import torch from torch.optim.lr_scheduler import LambdaLR def warmup_cosine(optimizer, warmup_iters500, total_iters20000, base_lr0.001): def lr_lambda(step): if step warmup_iters: return step / warmup_iters progress (step - warmup_iters) / (total_iters - warmup_iters) return 0.5 * (1 torch.cos(torch.tensor(3.14159 * progress))) return LambdaLR(optimizer, lr_lambda) # 使用示例 optimizer torch.optim.SGD(model.parameters(), lr0.001, momentum0.9, weight_decay0.0001) scheduler warmup_cosine(optimizer, warmup_iters500, total_iters20000)参数说明warmup_iters设 500 是因为 batch size 为 4 时500 个 iteration 大约对应 2000 张图足够让模型看到各类遮挡样本。total_iters根据你的数据集大小算一般训 20 到 30 个 epoch。base_lr如果 loss 在前 1000 iteration 还在涨就降到 0.0005。4.2 分类损失和回归损失的权重怎么调Faster-RCNN 的总损失是rpn_cls_loss rpn_bbox_loss roi_cls_loss roi_bbox_loss。默认权重都是 1.0但在遮挡场景下分类损失容易主导因为遮挡样本的类别边界模糊模型倾向于把遮挡框判成背景。这时候可以把roi_cls_loss的权重降到 0.8把roi_bbox_loss提到 1.2让模型更关注框的回归精度。另一个技巧是用 focal loss 替换交叉熵做分类。Focal loss 的gamma设 2.0alpha设 0.25能缓解正负样本不平衡。但注意 focal loss 对学习率更敏感用了之后初始学习率要降到 0.0005。4.3 评估协议mAP 在 Easy、Medium、Hard 子集上的差异WIDER FACE 的评估分 Easy、Medium、Hard 三个子集划分依据是检测难度和遮挡程度不完全对应但 Hard 子集里遮挡样本占比最高。你的毕业设计文档里必须写清楚 mAP 是在哪个子集上算的否则答辩时说不清。我一般会三个子集都跑重点看 Hard 子集的 AP。如果 Easy 和 Medium 的 AP 都在 0.9 以上Hard 只有 0.6说明模型对遮挡的泛化不够需要回去检查增强策略和 anchor 设置。评估脚本用官方提供的eval_tools注意里面的 IoU 阈值是 0.5和 COCO 的 0.5:0.95 不一样别搞混。注意评估时要把difficult框排除否则 mAP 会偏高。官方评估脚本默认会处理这个字段但如果你自己改了 XML要确认difficult标记正确。5. 避坑与排查遮挡人脸检测训练中最容易翻车的 5 个点5.1 现象训练 loss 正常下降但验证 mAP 始终为 0原因通常是类别标签映射错了。VOC 格式里name字段写的是face但你的代码里类别列表可能是[background, face]索引对不上。或者评估时把预测框的类别索引当成了 0背景导致所有预测都被滤掉。解决在数据集类的__getitem__里打印一次target[labels]确认值在 1 到 num_classes-1 之间。评估前先可视化几张预测结果看看框有没有画出来。5.2 现象RPN 的召回率很低大量人脸在 proposal 阶段就丢了原因可能是 anchor 尺度和数据集不匹配或者 RPN 的pre_nms_top_n设太小。默认pre_nms_top_n是 2000如果图片里人脸密集2000 个 proposal 不够用。解决先把pre_nms_top_n提到 4000post_nms_top_n从 300 提到 500。如果召回率还上不去回去检查 anchor 聚类结果把最小 anchor 的尺度再降一档。5.3 现象训练到第 5 个 epoch 突然 loss 变 NaN原因通常是学习率太大或者梯度爆炸。遮挡样本的 loss 本身波动大如果没加梯度裁剪某个 batch 的梯度范数可能冲到几百。解决在optimizer.step()前加torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm10.0)。同时检查学习率如果用的是 0.004 以上降到 0.001 再试。5.4 现象验证集 AP 比训练集低 20 个点以上原因可能是过拟合也可能是训练集和验证集的遮挡分布不一致。如果你把严重遮挡的样本都分到了训练集验证集里全是简单样本AP 反而会虚高反过来验证集里全是难例AP 就会很低。解决划分数据集时按occlusion字段分层采样保证三个集合的遮挡比例接近。另外增强只加在训练集验证集用原图。5.5 现象推理时单张图耗时超过 500ms达不到实时原因可能是输入分辨率太大或者 backbone 用了 ResNet-50 且没做推理优化。毕业设计里如果要求实时输入可以降到 320×320backbone 换 MobileNetV2再把sampling_ratio降回 2。解决用torch.no_grad()包住推理代码关掉梯度计算。如果还慢把 NMS 的阈值从 0.5 提到 0.6减少保留框数量。6. 把 mAP 从 0.62 拉到 0.71 的一个具体技巧遮挡感知的 RoI 特征重加权最后一章讲一个我在实际调参中验证有效的技巧不复杂但需要对 RoIAlign 的输出做一点后处理。思路是遮挡人脸的 RoI 特征里被遮挡区域的特征响应弱如果直接送进分类头弱响应会被平均掉。我们可以根据特征图的响应强度给每个空间位置算一个权重让模型更关注未遮挡区域。具体做法是在 RoIHead 里加一个轻量的注意力模块。输入是 RoIAlign 输出的 7×7×256 特征先过一个 1×1 卷积降到 128 通道再用一个 3×3 卷积算空间注意力图最后用 sigmoid 归一化后乘回原特征。这个模块参数量不到 0.1M训练时和主网络一起端到端学。import torch.nn as nn import torch.nn.functional as F class OcclusionAwareRoI(nn.Module): def __init__(self, in_channels256, reduction128): super().__init__() self.conv1 nn.Conv2d(in_channels, reduction, 1) self.conv2 nn.Conv2d(reduction, 1, 3, padding1) self.bn nn.BatchNorm2d(reduction) def forward(self, roi_features): # roi_features: [N, 256, 7, 7] attn F.relu(self.bn(self.conv1(roi_features))) attn torch.sigmoid(self.conv2(attn)) # [N, 1, 7, 7] return roi_features * attn # 在 RoIHead 的 forward 里插入 # roi_feat self.roi_align(features, proposals) # roi_feat self.occlusion_aware(roi_feat) # cls_score self.fc_cls(roi_feat.flatten(1))参数说明reduction设 128 是压缩比如果显存够可以设 256但收益不明显。conv2的卷积核是 3×3padding 为 1保持空间尺寸不变。注意力图用 sigmoid 而不是 softmax因为每个位置独立判断是否被遮挡不需要竞争。我在这套配置下WIDER FACE Hard 子集的 AP 从 0.62 提到了 0.71提升主要来自严重遮挡样本的召回。代价是单张推理耗时增加了约 8ms可以接受。如果你也想试这个技巧建议先跑通 baseline确认 mAP 在 0.6 左右再加这个模块做消融这样文档里能写出“加了什么、涨了多少”的对比表。最后说个血泪经验毕业设计的文档里训练命令、配置文件、评估脚本这三样必须齐全而且要和代码仓库里的版本一致。我见过有人文档里写的是lr0.001代码里实际是0.004答辩时被老师翻出来场面很尴尬。把 config 文件单独存一份每次改参数都记下来最后整理成表格放进附录。希望帮到你。本文还有配套的精品资源点击获取
返回列表