https://github.com/wyrcode/mtcnn
MTCNN由3个网络结构组成(P-Net,R-Net,O-Net)。
Proposal Network (P-Net):该网络结构主要获得了人脸区域的候选窗口和边界框的回归向量。并用该边界框做回归,对候选窗口进行校准,然后通过非极大值抑制(NMS)来合并高度重叠的候选框。
Refine Network (R-Net):该网络结构还是通过边界框回归和NMS来去掉那些false-positive区域。
只是由于该网络结构和P-Net网络结构有差异,多了一个全连接层,所以会取得更好的抑制false-positive的作用。
Output Network (O-Net):该层比R-Net层又多了一层卷基层,所以处理的结果会更加精细。作用和R-Net层作用一样。但是该层对人脸区域进行了更多的监督,同时还会输出5个坐标(landmark)。
为了保证5个特征点的精准,在最后面又加了一层Lnet,对人脸的5个特征点进一步的回归修正。
如同下面检测代码的流程
vector MtcnnDetector::Detect(ncnn::Mat img)
{
int img_w = img.w;
int img_h = img.h;
vector pnet_results = Pnet_Detect(img);//将图片放入Pnet中。得到一批人脸候选框
doNms(pnet_results, 0.7, "union");//通过非极大值抑制,清除一些正交比过大的候选框,0.7为正交比的阈值
refine(pnet_results, img_h, img_w, true);//将框的坐标通过回归修正
vector rnet_results = Rnet_Detect(img, pnet_results);//将人脸候选框信息输入Rnet,输出更加精细的人脸候选框信息
doNms(rnet_results, 0.7, "union");
refine(rnet_results, img_h, img_w, true);
vector onet_results = Onet_Detect(img, rnet_results);//将人脸候选框信息输入Onet,输出的精细的5个人脸的特征点。
refine(onet_results, img_h, img_w, false);
doNms(onet_results, 0.7, "min");
Lnet_Detect(img, onet_results);//进一步的对采集到的5个特征点坐标进行回归修正
return onet_results;
}
上面代码中的回归修正函数的详细讲解可以参考我前面的博客:目标检测中的候选框的回归修正c++版
Proposal Network (P-Net):该网络结构主要获得了人脸区域的候选窗口和边界框的回归向量。并用该边界框做回归,对候选窗口进行校准,然后通过非极大值抑制(NMS)来合并高度重叠的候选框。
1。首先生成图片金字塔,因为一张图片中的人脸大小不一定,所以将图片大小按比例缩小成多张图片,设置了最小的检测区域为12
这样缩小后的图片中的人脸,就可以被检测到了。
输入图片的尺寸,minsize和factor共同影响了图像金字塔的阶层数。也就是说决定能够生成多少张图。缩放后的尺寸minL=org_L*(12/minisize)*factor^(n),n={0,1,2,3,…,N},缩放尺寸最小不能小于12,也就是缩放到12为止。n的数量也就是能够缩放出图片的数量。
2.以前的是通过滑动窗口来截取图片然后送入网络检测。现在是用卷积代替了滑动窗口,并通过卷积之后的结果,定位原始的检测区域。
网络定义的nput的size是12 * 12* 3,由于Pnet只有卷积层,我们可以直接将resize后的图像给网络进行前传,只是得到的结果不是1* 1* 2和1* 1* 4,而是m* m* 2和m* m* 4。这样就不用先从resize的图上滑动截取各种12* 12* 3的图进入网络,而是一次性送入通过卷积,在根据结果回推每个结果对应的12* 12的图在输入图的什么位置。利用的就是卷积来代替原来的滑动窗口。
3.针对金字塔中每张图,网络forward计算后都得到了人脸得分以及人脸框回归的结果。人脸分类得分是两个通道的三维矩阵m* m* 2,其实对应在网络输入图片上m* m个12* 12的滑框,结合当前图片在金字塔图片中的缩放scale,可以推算出每个滑框在原始图像中的具体坐标。
4.首先要根据得分进行筛选,得分低于阈值的滑框,排除。
当金字塔中所有图片处理完后,然后利用nms非极大值抑制,对剩下的滑框进行合并。然后利用最后剩余的滑框对应的Bbox结果转换成原始图像中像素坐标,也就是得到了人脸框的坐标。
nms具体解释,可以参照我的博客:NMS非极大值抑制,
候选框的生成的详细过程解释,可以参照我的博客:MTCNN中 Pnet候选框生成算法
生成候选框的算法可参考我的前一篇博客。
通过代码来学习Pnet的工作原理
vector MtcnnDetector::Pnet_Detect(ncnn::Mat img)
{
vector results;
int img_w = img.w;
int img_h = img.h;
float minl = img_w < img_h ? img_w : img_h;
double scale = 12.0 / this->minsize;//12为最小检测的大小
minl *= scale;
vector scales;
while (minl > 12)
{
scales.push_back(scale);
minl *= this->factor;
scale *= this->factor;
}//得到一系列的缩小的比例,根据比例缩小的每一张图片输入Pnet网络中
for (auto it = scales.begin(); it != scales.end(); it++)
{
scale = (double)(*it);
int hs = (int) ceil(img_h * scale);
int ws = (int) ceil(img_w * scale);//缩小宽高
ncnn::Mat in = resize(img, ws, hs);
in.substract_mean_normalize(this->mean_vals, this->norm_vals);
ncnn::Extractor ex = Pnet.create_extractor();
ex.set_light_mode(true);
ex.input("data", in);
ncnn::Mat score;
ncnn::Mat location;
ex.extract("prob1", score);//判断各区域是否有人脸的概率,输出维度为m*m*2
ex.extract("conv4_2", location);//判断各回归框的修正信息,输出维度为m*m*4
//输出信息可参考下面的网络结构
vector bboxs = generateBbox(score, location, *it, this->threshold[0]);//根据阈值和Pnet网络的输出,生成候选框
doNms(bboxs, 0.5, "union");//非极大值抑制
results.insert(results.end(), bboxs.begin(), bboxs.end());
}
return results;
}
该网络结构还是通过边界框回归和NMS来去掉那些false-positive区域。
只是由于该网络结构和P-Net网络结构有差异,多了一个全连接层,所以会取得更好的抑制false-positive的作用。
代码中基本同Pnet一样
vector MtcnnDetector::Rnet_Detect(ncnn::Mat img, vector bboxs)
{
vector results;
int img_w = img.w;
int img_h = img.h;
for (auto it = bboxs.begin(); it != bboxs.end(); it++)
{
ncnn::Mat img_t;
copy_cut_border(img, img_t, it->y[0], img_h - it->y[1], it->x[0], img_w - it->x[1]);
ncnn::Mat in = resize(img_t, 24, 24);
in.substract_mean_normalize(this->mean_vals, this->norm_vals);
ncnn::Extractor ex = Rnet.create_extractor();
ex.set_light_mode(true);
ex.input("data", in);
ncnn::Mat score, bbox;
ex.extract("prob1", score);
ex.extract("conv5_2", bbox);
if ((float)score[1] > threshold[1])//若人脸候选框的置信度大于阈值,则对候选框进修正
{
for (int c = 0; c < 4; c++)
{
it->regreCoord[c] = (float)bbox[c];
}
it->score = (float)score[1];
results.push_back(*it);
}
}
return results;
}
该层比R-Net层又多了一层卷基层,所以处理的结果会更加精细。作用和R-Net层作用一样。但是该层对人脸区域进行了更多的监督,同时还会输出5个坐标(landmark)。
vector MtcnnDetector::Onet_Detect(ncnn::Mat img, vector bboxs)
{
vector results;
int img_w = img.w;
int img_h = img.h;
for (auto it = bboxs.begin(); it != bboxs.end(); it++)
{
ncnn::Mat img_t;
copy_cut_border(img, img_t, it->y[0], img_h - it->y[1], it->x[0], img_w - it->x[1]);
ncnn::Mat in = resize(img_t, 48, 48);
in.substract_mean_normalize(this->mean_vals, this->norm_vals);
ncnn::Extractor ex = Onet.create_extractor();
ex.set_light_mode(true);
ex.input("data", in);
ncnn::Mat score, bbox, point;
ex.extract("prob1", score);
ex.extract("conv6_2", bbox);
ex.extract("conv6_3", point);
if ((float)score[1] > threshold[2])
{
for (int c = 0; c < 4; c++)
{
it->regreCoord[c] = (float)bbox[c];
}
for (int p = 0; p < 5; p++)
{
it->landmark[2 * p] = it->x[0] + (it->x[1] - it->x[0]) * point[p];
it->landmark[2 * p + 1] = it->y[0] + (it->y[1] - it->y[0]) * point[p + 5];
}
it->score = (float)score[1];
results.push_back(*it);
}
}
return results;
}
Lnet,对人脸的5个特征点进一步的回归修正。
void MtcnnDetector::Lnet_Detect(ncnn::Mat img, vector &bboxes)
{
int img_w = img.w;
int img_h = img.h;
for (auto it = bboxes.begin(); it != bboxes.end(); it++)
{
int w = it->x[1] - it->x[0] + 1;
int h = it->y[1] - it->y[0] + 1;
int m = w > h ? w : h;
m = (int)round(m * 0.25);
if (m % 2 == 1) m++;
m /= 2;
ncnn::Mat in(24, 24, 15);
for (int i = 0; i < 5; i++)
{
int px = it->landmark[2 * i];
int py = it->landmark[2 * i + 1];
ncnn::Mat cut;
copy_cut_border(img, cut, py - m, img_h - py - m, px - m, img_w - px - m);
ncnn::Mat resized = resize(cut, 24, 24);
resized.substract_mean_normalize(this->mean_vals, this->norm_vals);
for (int j = 0; j < 3; j++)
memcpy(in.channel(3 * i + j), resized.channel(j), 24 * 24 * sizeof(float));
}
ncnn::Extractor ex = Lnet.create_extractor();
ex.set_light_mode(true);
ex.input("data", in);
ncnn::Mat out1, out2, out3, out4, out5;
ex.extract("fc5_1", out1);
ex.extract("fc5_2", out2);
ex.extract("fc5_3", out3);
ex.extract("fc5_4", out4);
ex.extract("fc5_5", out5);
if (abs(out1[0] - 0.5) > 0.35) out1[0] = 0.5f;
if (abs(out1[1] - 0.5) > 0.35) out1[1] = 0.5f;
if (abs(out2[0] - 0.5) > 0.35) out2[0] = 0.5f;
if (abs(out2[1] - 0.5) > 0.35) out2[1] = 0.5f;
if (abs(out3[0] - 0.5) > 0.35) out3[0] = 0.5f;
if (abs(out3[1] - 0.5) > 0.35) out3[1] = 0.5f;
if (abs(out4[0] - 0.5) > 0.35) out4[0] = 0.5f;
if (abs(out4[1] - 0.5) > 0.35) out4[1] = 0.5f;
if (abs(out5[0] - 0.5) > 0.35) out5[0] = 0.5f;
if (abs(out5[1] - 0.5) > 0.35) out5[1] = 0.5f;
it->landmark[0] += (int)round((out1[0] - 0.5) * m * 2);
it->landmark[1] += (int)round((out1[1] - 0.5) * m * 2);
it->landmark[2] += (int)round((out2[0] - 0.5) * m * 2);
it->landmark[3] += (int)round((out2[1] - 0.5) * m * 2);
it->landmark[4] += (int)round((out3[0] - 0.5) * m * 2);
it->landmark[5] += (int)round((out3[1] - 0.5) * m * 2);
it->landmark[6] += (int)round((out4[0] - 0.5) * m * 2);
it->landmark[7] += (int)round((out4[1] - 0.5) * m * 2);
it->landmark[8] += (int)round((out5[0] - 0.5) * m * 2);
it->landmark[9] += (int)round((out5[1] - 0.5) * m * 2);
}
}
包括回归框的生成,非极大值抑制nms,框的回归修正refin。具体的函数解释请参考我前面的博客。
MTCNN特征描述子主要包含3个部分,人脸/非人脸分类器,边界框回归,地标定位。
人脸分类:
上式为人脸分类的交叉熵损失函数,其中,pi为是人脸的概率,yidet为背景的真实标签。
上式为通过欧氏距离计算的回归损失。其中,带尖的y为通过网络预测得到,不带尖的y为实际的真实的背景坐标。其中,y为一个(左上角x,左上角y,长,宽)组成的四元组。
和边界回归一样,还是计算网络预测的地标位置和实际真实地标的欧式距离,并最小化该距离。其中,,带尖的y为通过网络预测得到,不带尖的y为实际的真实的地标坐标。由于一共5个点,每个点2个坐标,所以,y属于十元组。