第3章 深度学习检测器:MTCNN 原理详解

P-Net / R-Net / O-Net 结构 · 训练与推理流程

1. MTCNN 概述

MTCNN (Multi-task Cascaded Convolutional Networks) 是一种经典的人脸检测与对齐框架,由三个级联的轻量网络组成:P-Net (Proposal Network)、R-Net (Refine Network) 和 O-Net (Output Network)。它同时输出人脸边界框、关键点(眼睛、鼻子、嘴角)坐标,并利用级联结构实现由粗到精的检测。

核心思想: 通过三个阶段的网络逐步筛选候选框,每个阶段都进行边界框回归和关键点回归,同时利用非极大值抑制 (NMS) 去除冗余框。
P-Net Proposal 12×12 全卷积 R-Net Refine 24×24 分类+回归 O-Net Output 48×48 精细输出 NMS + 回归 NMS + 回归 最终检测框 + 关键点

2. P-Net / R-Net / O-Net 结构

三个网络均包含卷积层、PReLU 激活和全连接层(或卷积等价),但输入尺寸和深度不同。下表总结了核心差异:

网络输入尺寸主要结构输出任务
P-Net12×12×3全卷积 (FCN),3 个卷积层人脸分类 (2)、边界框回归 (4)、关键点回归 (10)
R-Net24×24×3卷积 + 全连接层,4 个卷积层 + 2 FC人脸分类 (2)、边界框回归 (4)、关键点回归 (10)
O-Net48×48×3更深卷积 + 全连接,5 个卷积层 + 3 FC人脸分类 (2)、边界框回归 (4)、关键点回归 (10)

P-Net 结构示意

12×12×3 Conv 3×3 MP 2×2 Conv 3×3 MP 2×2 Conv 3×3 1×1 卷积 分类 2 回归 4 关键点 10

3. 训练流程与多任务损失

MTCNN 使用三个任务联合训练:人脸分类、边界框回归、关键点定位。总损失函数为:

L = α · Lcls + β · Lbox + γ · Llandmark
其中 α, β, γ 为任务权重(通常 α=1, β=0.5, γ=0.5)。分类损失使用交叉熵,回归损失使用欧氏距离。

训练数据采样策略: 每个 mini-batch 包含正样本(IoU > 0.65)、负样本(IoU < 0.3)、部分样本(0.3 < IoU < 0.65)和关键点样本,比例约为 1:3:1:2。

图像金字塔 P-Net 推理 NMS + 回归 R-Net / O-Net 多尺度输入 候选框 精炼框 最终检测

4. 推理流程

  1. 图像金字塔构建: 对输入图像进行多尺度缩放,生成图像金字塔,最小人脸尺寸通常设为 12×12。
  2. P-Net 阶段: 全卷积网络快速扫描所有尺度,输出大量候选框,经 NMS (阈值 0.7) 和边界框回归后保留约 1000 个框。
  3. R-Net 阶段: 将候选框缩放到 24×24 输入 R-Net,进一步分类和回归,NMS 后保留约 100 个高置信度框。
  4. O-Net 阶段: 将框缩放到 48×48 输入 O-Net,输出最终人脸框、置信度以及 5 个关键点坐标,最后 NMS (阈值 0.6) 得到最终结果。
图像金字塔 P-Net NMS + 回归 R-Net O-Net 输出

5. 关键点回归与代码片段

MTCNN 输出 5 个关键点:左眼、右眼、鼻子、左嘴角、右嘴角。训练时使用归一化坐标。以下为 P-Net 前向推理的简化代码:

# P-Net 推理示例 (PyTorch 风格)
import torch
import torch.nn as nn

class PNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 10, 3, 1)
        self.prelu1 = nn.PReLU(10)
        self.pool1 = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(10, 16, 3, 1)
        self.prelu2 = nn.PReLU(16)
        self.conv3 = nn.Conv2d(16, 32, 3, 1)
        self.prelu3 = nn.PReLU(32)
        # 输出分支
        self.cls = nn.Conv2d(32, 2, 1, 1)
        self.bbox = nn.Conv2d(32, 4, 1, 1)
        self.landmark = nn.Conv2d(32, 10, 1, 1)

    def forward(self, x):
        x = self.pool1(self.prelu1(self.conv1(x)))
        x = self.prelu2(self.conv2(x))
        x = self.prelu3(self.conv3(x))
        cls = torch.softmax(self.cls(x), dim=1)
        bbox = self.bbox(x)
        landmark = self.landmark(x)
        return cls, bbox, landmark