资讯详情

PyTorch实现MTCNN级联人脸检测与关键点对齐实战教程

📅 2026/9/16 1:59:47 | 华诺云谱 👁 阅读
PyTorch实现MTCNN级联人脸检测与关键点对齐实战教程
简介基于 PyTorch 深度学习的人脸检测与识别系统是一份评分高达 95 分以上的毕业设计完整源码主要面向计算机、Python 相关专业的学生与从业者适用于毕业设计、期末课程设计、课程大作业等场景。系统涵盖数据准备、模型训练、网络配置、测试推理等完整环节代码经过严格调试可直接运行帮助使用者省去环境适配与排错时间。资源共 50 个文件以 Python 源码为主37 个 py另包含 3 个模型权重文件、覆盖 macOS 与 Windows 的环境依赖配置yml/yaml、说明文本与示例图片压缩包大小约 3.71MB目录按数据准备、模型训练、测试等子模块划分清晰便于定位与二次开发。该资源已有 644 人学习适合需要快速搭建人脸检测识别项目、参考完整毕设实现或深入理解深度学习人脸识别流程的读者。通过阅读源码和配置文件可掌握人脸检测、模型训练验证、跨平台环境部署等关键技术点具有较高学习价值与实践参考意义。1. 为什么人脸检测要先跑三遍网络从 MTCNN 级联结构说起人脸检测在真实场景里比很多人想象的更难同一张画面里小脸、侧脸、遮挡同时出现单阶段检测器要么快而不准要么准而不够快。这套基于 PyTorch 的深度学习项目采用的 MTCNN 级联结构把问题拆成三次过滤——先用极轻量的 PNet 在全图滑窗再用 RNet 精修候选框最后用 ONet 同时输出人脸框和五个关键点。对于准备毕业设计或课程设计的人来说它最有价值的地方不只是 model_store 里三个可以直接运行的 pt 权重而是 dface 目录下数据准备、训练、推理的完整闭环。跟着 test_image.py 走一遍基本就能弄懂人脸检测和人脸识别之间那条真正的分界线。2. 拆解 PyTorch 工程dface 里的数据标注、PNet 结构与多任务损失压缩包解压后根目录的 environment*.yaml 是 conda 环境描述model_store 里是 PNet、RNet、ONet 三个权重文件dface 目录下则是核心代码。wider_origin_anno.txt 是 WIDER FACE 的原始标注anno_store 保存 prepare_data 处理后的样本prepare_data 模块负责把标注转成三个网络各自需要的训练数据。很多人在拿到这个工程时习惯先跑 test_image.py但如果想改训练流程最该读的是 prepare_data 和 config.py。2.1 WIDER FACE 标注与三类样本的采样逻辑WIDER FACE 的原始标注格式比较直观每张图片一行路径下一行是框数量再往下每一行是一个人脸框附带属性。prepare_data 要做的事情是随机生成大量候选窗口计算窗口与真实人脸框的 IoU再按照阈值给窗口打标签。MTCNN 之所以要单独区分“部分样本”是因为 PNet 和 RNet 除了做人脸分类还要回归边框偏移和关键点部分样本的 IoU 不算高但也不低在分类任务里能当作困难样本让网络更稳定同时它们包含未被切碎的人脸结构可以让框回归学得更稳。prepare_data 在生成样本时还会记录每个样本的人脸框标签、关键点标签以及它们在原始图上的坐标后续 train_net.py 才知道该往哪个分支回传。网络输入分辨率正样本 IoU部分样本 IoU负样本 IoU框回归样本关键点回归样本PNet12x12 0.650.4 ~ 0.65 0.3正样本 部分样本仅正样本RNet24x24 0.650.4 ~ 0.65 0.3正样本 部分样本仅正样本ONet48x48 0.650.4 ~ 0.65 0.3正样本 部分样本仅正样本注意关键点回归只使用正样本因为只有 IoU 较高时裁剪窗口里的五官位置才是完整可用的。如果让部分样本也参与关键点回归ONet 训练早期 loss 会因为边界切掉眼眉而剧烈震荡。prepare_data 也需要单独跑三次分别生成 PNet、RNet、ONet 对应分辨率的样本三个网络的输入分布必须和推理时保持一致。2.2 PNet 的 PyTorch 结构三个卷积分支合一PNet 是整个系统的第一道闸门输入既可以是单张 12x12 小图也可以是整张金字塔缩放图。它本质是一个小型全卷积网络卷积层后没有全连接所以分辨率只要大于等于 12就能一次性输出多个位置的预测。下面这段代码展示了标准 PNet 的 PyTorch 写法和 dface/core 下 models 里的实现是等价的。import torch import torch.nn as nn class PNet(nn.Module): def __init__(self): super(PNet, self).__init__() self.pre_layer nn.Sequential( nn.Conv2d(3, 10, kernel_size3, stride1), # 12x12 - 10x10 nn.PReLU(), nn.MaxPool2d(kernel_size2, stride2), # 10x10 - 5x5 nn.Conv2d(10, 16, kernel_size3, stride1), # 5x5 - 3x3 nn.PReLU(), nn.Conv2d(16, 32, kernel_size3, stride1), # 3x3 - 1x1 nn.PReLU(), ) self.cls_branch nn.Conv2d(32, 2, kernel_size1) # 人/非人 self.bbox_branch nn.Conv2d(32, 4, kernel_size1) # 框偏移相对 12x12 self.landmark_branch nn.Conv2d(32, 10, kernel_size1) # 5 个关键点 def forward(self, x): feat self.pre_layer(x) return self.cls_branch(feat), self.bbox_branch(feat), self.landmark_branch(feat)这里卷积层都没有 padding尺寸靠 MaxPool2d 压缩。第一个 MaxPool 步长为 2是整个网络唯一的降采样点推理时反算候选框在原图的位置也要靠它推算总 stride。forward 返回的三个分支共享前面全部参数梯度会同时更新主干和所有分支。关键点分支输出 10 个数按两个坐标一组顺序是左眼、右眼、鼻尖、左嘴角、右嘴角。2.3 多任务损失和 train_net.py 训练入口MTCNN 的损失由三部分构成分类交叉熵、边框回归 MSE、关键点回归 MSE。不同任务不能等权相加因为分类 loss 的绝对量级通常比回归 loss 大不少项目里常见做法是把回归损失权重设成 0.5分类权重保持 1这样边框和关键点分支不会被分类分支完全压住。import torch.nn.functional as F def mtcnn_loss(cls_prob, labels, bbox_pred, bbox_target, landmark_pred, landmark_target): # labels: 0负样本, 1正样本, 2部分样本 # 分类分支只统计负样本和正样本部分样本不参与分类 cls_mask labels 2 cls_loss F.cross_entropy(cls_prob[cls_mask], labels[cls_mask]) # 框回归使用正样本和部分样本 bbox_mask labels 0 bbox_loss F.mse_loss(bbox_pred[bbox_mask], bbox_target[bbox_mask], reductionmean) # 关键点回归只用正样本 landmark_mask labels 1 landmark_loss F.mse_loss(landmark_pred[landmark_mask], landmark_target[landmark_mask], reductionmean) return cls_loss 0.5 * bbox_loss 0.5 * landmark_loss代码里假设 cls_prob、bbox_pred、landmark_pred 已经按 batch 展开成 [N, C]。分类分支把部分样本排除避免把 IoU 不到 0.65 的窗口硬当成人脸学习框回归分支包含部分样本是因为这部分样本虽然分类置信度不高但框坐标信息仍然有效。train_net.py 的启动方式一般是python train_net.py --network pnet --data_dir ./anno_store --model_store ./model_store--network选择当前训练 pnet、rnet 还是 onet--data_dir指向 prepare_data 的输出目录--model_store指定权重保存位置。如果你想接着已有的权重继续训练可以加--pretrained_model参数指向 pnet_epoch.pt避免从随机初始化开始。3. 环境复现与 test_image.py 推理从 environment.yml 到模型输出这个工程一共给出了三份环境文件environment.yml、environment-win64.yml、environment_osx.yaml分别对应通用、Windows、macOS。Windows 上直接用 environment-win64.yml 创建环境能省下不少 pip 依赖冲突的时间。conda env create -f environment-win64.yml conda activate dface python test_image.py --image test.jpg如果 conda 解析太慢也可以手动创建最小依赖环境conda create -n dface python3.8 conda activate dface conda install pytorch torchvision cpuonly -c pytorch pip install opencv-python numpy matplotlib具体版本以 environment-win64.yml 里锁定的为准这里只保证能跑通推理。PyTorch CPU 版本处理 test.jpg 单张图片完全够用但训练 RNet、ONet 时建议换成 GPU 版。3.1 图像金字塔参数与候选框反算detect_face 的入口参数有三个最影响效果minsize、threshold、factor。minsize 表示能被检测到的人脸最小边长threshold 是三级网络的置信度阈值factor 是图像金字塔缩放系数。常见配置是 minsize20 到 40threshold[0.6, 0.7, 0.7]factor0.709。factor 取 0.709 是因为 sqrt(0.5) 约等于 0.707MTCNN 原文希望每层面积减少一半实际工程里微调到 0.709 可以避免浮点误差导致某些层重复生成。import cv2 import numpy as np def build_pyramid_scales(img, minsize40, factor0.709): h, w img.shape[:2] min_side min(h, w) scale 12.0 / minsize min_side * scale scales [] while min_side 12: scales.append(scale) scale * factor min_side * factor return scalesminsize 越小scales 层数越多小脸检出率越高但整张图推理时间接近线性上涨。接下来需要在每个尺度上跑 PNet并把 feature map 上的坐标反算回原图for scale in scales: new_w, new_h int(w * scale), int(h * scale) resized cv2.resize(img, (new_w, new_h)) # 如果模型按 RGB 训练这里需要先 cv2.cvtColor 转成 RGB tensor torch.from_numpy(resized).permute(2, 0, 1).unsqueeze(0).float() / 255.0 with torch.no_grad(): cls, bbox, _ pnet(tensor) score torch.softmax(cls, dim1)[0, 1].cpu().numpy() # PNet 总 stride212x12 窗口中心在 feature map 上的坐标 pts np.argwhere(score threshold[0]) for y, x in pts: x1 (x * 2) / scale y1 (y * 2) / scale x2 (x * 2 12) / scale y2 (y * 2 12) / scale # 再用 bbox 分支预测的偏移修正 x1, y1, x2, y2这里 x*2 是步长12 是 PNet 输入窗口大小。bbox 分支输出的 4 个偏移是经过归一化的真正的显示框需要在 x1、y1 基础上把偏移乘上 12 再除以 scale具体公式参考 dface 里的 convert_to_square 函数。3.2 NMS 与 RNet/ONet 的级联过滤PNet 输出的框会互相重叠需要用 NMS 合并。PNet 阶段的 NMS IoU 阈值一般设 0.5也就是说两个候选框 IoU 超过 0.5 时只保留置信度更高的那个。随后把保留下来的框从原图中裁剪出来缩放成 24x24送进 RNetRNet 的置信度阈值提到 threshold[1]再做一次 NMS。ONet 阶段除了最终置信度还会输出关键点坐标画图时直接把 landmark 分支的前 10 个值 reshape 成 5 个点。阶段输入置信度阈值NMS IoU 阈值主要输出PNet金字塔缩放图0.60.5大量候选框RNet24x24 裁剪图0.70.7精修框ONet48x48 裁剪图0.70.7最终框 五点坐标实践里 RNet、ONet 的 NMS 阈值往往从 0.5 调到 0.7因为经过上一级过滤后框已经比较准阈值太低会误删同一张脸的两个高置信度框。如果你用 test_image.py 只是想在照片上画框可以把 ONet 输出的 bbox 和 landmark 直接画到原图。注意 ONet 输出的 landmark 是相对 48x48 裁剪窗口的画回原图前要先加裁剪窗口左上角坐标再除以缩放比例。这一步漏了的人画出来的关键点会跑到脸外面。3.3 权重加载时最常见的报错从 model_store 加载权重看起来很简单但经常会遇到 key 不匹配的问题net PNet() state torch.load(model_store/pnet_epoch.pt, map_locationcpu) net.load_state_dict(state) net.eval()如果 state 里的 key 带net.前缀load_state_dict 会报 unexpected key 或 missing key。处理方法是从每个 key 里把net.前缀去掉from collections import OrderedDict new_state OrderedDict() for k, v in state.items(): new_state[k.replace(net., , 1)] v net.load_state_dict(new_state)另一个坑是 device 不匹配。如果服务器有 GPUtorch.load 会把权重默认加载到显存而 test_image.py 里的输入 tensor 还在 CPU 上就会报 device mismatch。统一用map_locationcpu或者把输入 tensor 也.cuda()就能解决。4. 从检测到识别关键点对齐与 512 维特征的相似度匹配这个工程的核心是 MTCNN 检测但题目里的“识别系统”靠的是检测链路里最重要的副产品——五个关键点。ONet 输出关键点的意义就在于通过五点做仿射变换可以把不同角度、不同尺度的人脸统一映射到同一坐标系再交给特征提取网络做比对。没有这一步直接把人脸裁剪图送进识别模型同一个人的特征可能比不同人还远。4.1 用五点坐标做仿射对齐常见做法是把 ONet 输出的左眼、右眼、鼻尖、左嘴角、右嘴角映射到标准模板用 OpenCV 的 estimateAffinePartial2D 求变换矩阵再 warpAffine 得到 112x112 的对齐图。import cv2 import numpy as np REFERENCE_PTS np.array([ [38.2946, 51.6963], [73.5318, 51.5014], [56.0252, 71.7366], [41.5493, 92.3655], [70.7299, 92.2041] ], dtypenp.float32) def align_face(img, landmarks, output_size(112, 112)): # landmarks 来自 ONet顺序左眼、右眼、鼻尖、左嘴角、右嘴角 M, _ cv2.estimateAffinePartial2D(landmarks, REFERENCE_PTS) aligned cv2.warpAffine(img, M, output_size, borderValue0.0) return alignedestimateAffinePartial2D 返回的矩阵只包含旋转、缩放和平移不做透视变换能避免人脸变形。如果换成 160x160 的模板四个角点坐标按 160/112 等比缩放即可。注意对齐图的通道顺序要和识别模型训练时保持一致PyTorch 里通常需要把 OpenCV 的 BGR 结果转成 RGB并归一化到 [-1,1] 或 [0,1]。4.2 特征提取与相似度比较识别网络可以用 FaceNet、ArcFace 或 MobileFaceNet也可以先用 ImageNet 预训练的 ResNet 跑通流程。特征提取的重点是做 L2 归一化然后计算余弦相似度。import torch.nn.functional as F def get_feature(model, aligned_tensor): # aligned_tensor: (1, 3, 112, 112)已完成归一化 feat model(aligned_tensor) return F.normalize(feat, dim1).squeeze().cpu().numpy() def cosine_similarity(feat1, feat2): feat1 feat1 / np.linalg.norm(feat1) feat2 feat2 / np.linalg.norm(feat2) return float(np.dot(feat1, feat2))如果只是做 1:1 身份验证把两张对齐图的特征向量点乘即可如果要做 1:N 检索需要把所有注册人特征组织成矩阵用矩阵乘法一次性算出与所有人的相似度。识别系统的完整链路可以用这张表概括模块输入输出可选实现人脸检测原图人脸框 五点MTCNN也就是这个工程关键点对齐原图 五点112x112 对齐图OpenCV 仿射变换特征提取对齐图128~512 维归一化向量FaceNet / ArcFace / MobileFaceNet相似度计算两个特征向量0~1 分数余弦相似度4.3 识别阈值不能拍脑袋相似度阈值不能直接抄别人的。不同模型输出的特征分布不一样MobileFaceNet 在 0.5 附近效果最好ArcFace 可能到 0.4 就比较合适。正确做法是准备一组同一人的照片对和不同人的照片对画出 ROC 曲线取误识率小于 1% 时的阈值。人脸框在送入对齐前最好向外扩 1.2 倍再裁剪否则五个关键点太靠近边缘仿射变换容易把五官拉变形。关键点顺序也必须和识别模型训练时一致一般 InsightFace 系模型使用左眼、右眼、鼻尖、左嘴角、右嘴角换模型前先确认文档。5. 训练与验证PNet 不收敛时我检查的三个位置5.1 检查负样本比例是不是被污染了PNet 最常出现的问题是 loss 降不下去。不要急着改网络结构先看 prepare_data 生成的负样本占比。原论文推荐正负样本比例接近 1:3如果你在随机裁剪时把 IoU 阈值放太松负样本里会混入大量“半脸”分类分支就会持续震荡。检查方法是用 numpy 直接统计标签分布样本一般保存在 anno_store 下的 npy 文件里。python -c import numpy as np; dnp.load(anno_store/pnet_data.npy); print(np.bincount(d[:,0]))如果负样本比例不足回到 prepare_data 重新采样或者在线随机裁剪补负样本。注意负样本的 IoU 必须严格小于 0.3闭着眼睛卡阈值后期一定会出问题。5.2 确认 OHEM 只作用在分类分支MTCNN 训练里的在线难例挖掘经常被实现错。正确做法是对分类分支每个样本的 loss 排序取 loss 最高的前 70% 反传框回归和关键点回归仍然使用全部有效样本。如果源码里没有做可以在 train_net.py 里补一段cls_loss_per_sample F.cross_entropy(cls_prob, labels, reductionnone) keep_num int(cls_mask.sum() * 0.7) _, idx torch.topk(cls_loss_per_sample[cls_mask], keep_num) cls_loss cls_loss_per_sample[cls_mask][idx].mean()难例挖掘的作用是让网络持续关注那些“看着像人脸但容易分错”的窗口。很多人把三个任务的 loss 放在一起排序结果被框回归主导分类分支反而没有学到困难的分类样本。5.3 验证关键点坐标有没有被归一化打乱训练结束后用 test_image.py 对 test.jpg 跑一遍把 ONet 的 landmark 和 bbox 画出来。如果五个点落在眼睛、鼻尖、嘴角但人脸整体倾斜说明关键点坐标基本是准的如果点一直在乱跳大概率是 landmark 分支的输入标签没除以裁剪窗口的宽高。未归一化的 landmark_target 会让 loss 在不同尺度下差异巨大模型早期最稳的策略就是把 landmark 输出全部预测成零向量这也是关键点 loss 先降但可视化结果很差的原因。python -c import torch from dface.core.models import PNet net PNet() state torch.load(model_store/pnet_epoch.pt, map_locationcpu) print(params:, sum(p.numel() for p in net.parameters())) print(state keys:, len(state)) 用这段代码确认参数量和权重 key 数量能快速判断加载的是不是完整模型。权重数量对但关键点乱跳时回到 prepare_data 检查 landmark_target 的归一化过程而不是继续加大训练 epoch。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。