MTCNN,Multi-task convolutional neural network(多任务卷积神经网络),将人脸区域检测与人脸关键点检测放在了一起,基于cascade框架。总体可分为PNet、RNet、和ONet三层网络结构,
构建图像金字塔
首先将图像进行不同尺度的变换,构建图像金字塔,以适应不同大小的人脸的进行检测。
P-Net
全称为Proposal Network,其基本的构造是一个全连接网络。对上一步构建完成的图像金字塔,通过一个FCN进行初步特征提取与标定边框,并进行Bounding-Box Regression调整窗口与NMS进行大部分窗口的过滤。
R-Net
全称为Refine Network,其基本的构造是一个卷积神经网络,相对于第一层的P-Net来说,增加了一个全连接层,因此对于输入数据的筛选会更加严格。在图片经过P-Net后,会留下许多预测窗口,我们将所有的预测窗口送入R-Net,这个网络会滤除大量效果比较差的候选框,最后对选定的候选框进行Bounding-Box Regression和NMS进一步优化预测结果。
O-Net
全称为Output Network,基本结构是一个较为复杂的卷积神经网络,相对于R-Net来说多了一个卷积层。O-Net的效果与R-Net的区别在于这一层结构会通过更多的监督来识别面部的区域,而且会对人的面部特征点进行回归,最终输出五个人脸面部特征点。
FCN(全卷机网络)
全卷积网络就是去除了传统卷积网络的全连接层,然后对其进行反卷积对最后一个卷积层(或者其他合适的卷积层)的feature map进行上采样,使其恢复到原有图像的尺寸(或者其他),并对反卷积图像的每个像素点都可以进行一个类别的预测,同时保留了原有图像的空间信息。
同时,在反卷积对图像进行操作的过程中,也可以通过提取其他卷积层的反卷积结果对最终图像进行预测,合适的选择会使得结果更好、更精细。
IoU
对于某个图像的子目标图像和对这个子目标图像进行标定的预测框,把最终标定的预测框与真实子图像的自然框(通常需要人工标定)的某种相关性叫做IOU(Intersection over Union),经常使用的标准为两个框的交叉面积与合并面积之和。
Bounding-Box regression:
解决的问题:
当IOU小于某个值时,一种做法是直接将其对应的预测结果丢弃,而Bounding-Box regression的目的是对此预测窗口进行微调,使其接近真实值。
具体逻辑
在图像检测里面,子窗口一般使用四维向量(x,y,w,h)表示,代表着子窗口中心所对应的母图像坐标与自身宽高,目标是在前一步预测窗口对于真实窗口偏差过大的情况下,使得预测窗口经过某种变换得到更接近与真实值的窗口。
在实际使用之中,变换的输入输出按照具体算法给出的已经经过变换的结果和最终适合的结果的变换,可以理解为一个损失函数的线性回归。
NMS(非极大值抑制)
顾名思义,非极大值抑制就是抑制不是极大值的元素。在目标检测领域里面,可以使用该方法快速去掉重合度很高且标定相对不准确的预测框,但是这种方法对于重合的目标检测不友好。
Soft-NMS
对于优化重合目标检测的一种改进方法。核心在于在进行NMS的时候不直接删除被抑制的对象,而是降低其置信度。处理之后在最后统一一个置信度进行统一删除。
PRelu
在MTCNN中,卷积网络采用的激活函数是PRelu,带有参数的带有参数的Relu,相对于Relu滤除负值的做法,PRule对负值进行了添加参数而不是直接滤除,这种做法会给算法带来更多的计算量和更多的过拟合的可能性,但是由于保留了更多的信息,也可能是训练结果拟合性能更好。
金字塔的实现
首先是金字塔是如何实现的:
说白了就是对图像进行了resize的操作。因为里面没有全连接层 所以对于不同大小的输入图形得到的输出也是不一样的。但是输入的图像最小是1212 如果输入为1212 则对于该输入的结果会得到一个数字(或者两个数字,待会儿细说)该数字表明该12*12的图像是不是人脸的概率,该网络最后一层还有一个分支是输出4维度的回归数字 就是左上角和右下角的坐标。
不同的是否是人脸的输出标记
实现一:
来自:https://github.com/Sierkinhane/mtcnn-pytorch/blob/master/mtcnn/core/models.py
class PNet(nn.Module):
def __init__(self, is_train=False, use_cuda=True):
super(PNet, self).__init__()
self.is_train = is_train
self.use_cuda = use_cuda
# backend
self.pre_layer = nn.Sequential(
nn.Conv2d(3, 10, kernel_size=3, stride=1), # conv1
nn.PReLU(), # PReLU1
nn.MaxPool2d(kernel_size=2, stride=2), # pool1
nn.Conv2d(10, 16, kernel_size=3, stride=1), # conv2
nn.PReLU(), # PReLU2
nn.Conv2d(16, 32, kernel_size=3, stride=1), # conv3
nn.PReLU() # PReLU3
)
# detection
self.conv4_1 = nn.Conv2d(32, 1, kernel_size=1, stride=1)
# bounding box regresion
self.conv4_2 = nn.Conv2d(32, 4, kernel_size=1, stride=1)
# landmark localization
self.conv4_3 = nn.Conv2d(32, 10, kernel_size=1, stride=1)
# weight initiation with xavier
self.apply(weights_init)
def forward(self, x):
x = self.pre_layer(x)
label = F.sigmoid(self.conv4_1(x))
offset = self.conv4_2(x)
# landmark = self.conv4_3(x)
if self.is_train is True:
# label_loss = LossUtil.label_loss(self.gt_label,torch.squeeze(label))
# bbox_loss = LossUtil.bbox_loss(self.gt_bbox,torch.squeeze(offset))
return label,offset
#landmark = self.conv4_3(x)
return label, offset
实现二:
import torch.nn as nn
import torch.nn.functional as F
import math
class PNet(nn.Module):
def __init__(self):
super(PNet, self).__init__()
self.pre_layer = nn.Sequential(
nn.Conv2d(3, 10, kernel_size=3, stride=1), # conv1
nn.PReLU(), # PReLU1
nn.MaxPool2d(kernel_size=2, stride=2), # pool1
nn.Conv2d(10, 16, kernel_size=3, stride=1), # conv2
nn.PReLU(), # PReLU2
nn.Conv2d(16, 32, kernel_size=3, stride=1), # conv3
nn.PReLU() # PReLU3
)
# detection
self.conv4_1 = nn.Conv2d(32, 1, kernel_size=1, stride=1)
# bounding box regresion
self.conv4_2 = nn.Conv2d(32, 4, kernel_size=1, stride=1)
# weight initiation with xavier
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
def forward(self, x):
x = self.pre_layer(x)
label = F.sigmoid(self.conv4_1(x))
offset = self.conv4_2(x)
return label, offset
https://github.com/xiezheng-cs/mtcnn_pytorch/blob/master/models/pnet.py
实现三:
class PNet(nn.Module):
def __init__(self):
super(PNet, self).__init__()
self.model_path,_ = os.path.split(os.path.realpath(__file__))
self.features = nn.Sequential(OrderedDict([
('conv1', nn.Conv2d(3, 10, 3, 1)),
('prelu1', nn.PReLU(10)),
('pool1', nn.MaxPool2d(2, 2, ceil_mode=True)),
('conv2', nn.Conv2d(10, 16, 3, 1)),
('prelu2', nn.PReLU(16)),
('conv3', nn.Conv2d(16, 32, 3, 1)),
('prelu3', nn.PReLU(32))
]))
self.conv4_1 = nn.Conv2d(32, 2, 1, 1)
self.conv4_2 = nn.Conv2d(32, 4, 1, 1)
weights = np.load(os.path.join(self.model_path, 'weights', 'pnet.npy'))[()]
for n, p in self.named_parameters():
p.data = torch.FloatTensor(weights[n])
def forward(self, x):
x = self.features(x)
a = self.conv4_1(x)
b = self.conv4_2(x)
a = F.softmax(a, dim=1)
return b, a
来自:https://github.com/polarisZhao/mtcnn-pytorch/blob/master/src/model.py
其中一样的是卷积的结构的通道数与前几层的参数设置,不同的是最后一层的设计,对应论文中,应该是三个输出,有些只是两个输出,少了landmarks的信息,这里主要是论文也指出了前两个网络中的landmarks信息几乎不可用,没有输出也就算了。
但是对于是否是人脸这个问题上有些网络输出了一个通道,即给了一个值,来判断该区块是否是人脸。 有的是输出了两个通道,就是实现one-hot编码来实现的对判别该区块是否是人脸。怎么说呢,这是一个当成是分类问题,一个当成是回归问题了吧。但是更有意思的是对于输出两通道的那个代码,最后调用第一阶段的代码块如下:
def run_first_stage(image, net, scale, threshold):
"""
Run P-Net, generate bounding boxes, and do NMS.
"""
width, height = image.size
sw, sh = math.ceil(width*scale), math.ceil(height*scale)
img = image.resize((sw, sh), Image.BILINEAR)
img = np.asarray(img, 'float32')
img = torch.FloatTensor(_preprocess(img))
output = net(img)
probs = output[1].data.numpy()[0, 1, :, :]
offsets = output[0].data.numpy()
boxes = _generate_bboxes(probs, offsets, scale, threshold)
if len(boxes) == 0:
return None
keep = nms(boxes[:, 0:5], overlap_threshold=0.5)
return boxes[keep]
这里我们可以看到的是他竟然去了第二通道的数直接作为了概率,其实如果是作为softmax此处并不是不可以,但是这样的话,会对网络有什么影响,我们下一步再探究。
后两个网络为什么是固定输入
之前去面试的时候有人问我是什么限制的不同网络输入尺寸的大小。我也不知道怎么表达,但是的确我知道代码就在这里报错了:
class RNet(nn.Module):
''' RNet '''
def __init__(self,is_train=False, use_cuda=True):
super(RNet, self).__init__()
self.is_train = is_train
self.use_cuda = use_cuda
# backend
self.pre_layer = nn.Sequential(
nn.Conv2d(3, 28, kernel_size=3, stride=1), # conv1
nn.PReLU(), # prelu1
nn.MaxPool2d(kernel_size=3, stride=2), # pool1
nn.Conv2d(28, 48, kernel_size=3, stride=1), # conv2
nn.PReLU(), # prelu2
nn.MaxPool2d(kernel_size=3, stride=2), # pool2
nn.Conv2d(48, 64, kernel_size=2, stride=1), # conv3
nn.PReLU() # prelu3
)
self.conv4 = nn.Linear(64*2*2, 128) # conv4
self.prelu4 = nn.PReLU() # prelu4
# detection
self.conv5_1 = nn.Linear(128, 1)
# bounding box regression
self.conv5_2 = nn.Linear(128, 4)
# lanbmark localization
self.conv5_3 = nn.Linear(128, 10)
# weight initiation weih xavier
self.apply(weights_init)
def forward(self, x):
# backend
x = self.pre_layer(x)
x = x.view(x.size(0), -1)
x = self.conv4(x)
x = self.prelu4(x)
# detection
det = torch.sigmoid(self.conv5_1(x))
box = self.conv5_2(x)
# landmark = self.conv5_3(x)
if self.is_train is True:
return det, box
#landmard = self.conv5_3(x)
return det, box
这里引入了liner层,但是这一层的数据的输入和输出的维度实际上是已经固定了的。所以主要是这一个部分决定了输入的尺寸,因为经过卷积核和pooling以后尺寸是可以计算的,所以liner层的输入是已经固定下来的,但是输入图片的尺寸也因此固定下来了。因此对于Rnet和Onet来说这个问题完全没有必要了,因为在输入前就进行了预处理。
附记
竟然真有人看博客!!!
虽然注册CSDN也快十年了,在asp时代的脚本小子,php时代的看客,python时代的茫茫大军中的一员。真的在CSDN上留下的东西很少。惭愧惭愧,我自己看都不像是一个搞什么深度学习的人的博客。我得赶紧把笔记搬上来美化一下自己 哈哈哈哈