人臉識別系列三 | MTCNN算法詳解下篇

  • 2019 年 12 月 9 日
  • 筆記

前言

上篇講解了MTCNN算法的算法原理以及訓練細節,這篇文章主要從源碼實現的角度來解析一下MTCNN算法。我要解析的代碼來自github的https://github.com/ElegantGod/ncnn中的mtcnn.cpp。

網絡結構

再貼一下MTCNN的網絡結構,方便注釋代碼的時候可以隨時查看。

MTCNN代碼運行流程

在這裡插入圖片描述

代碼中的關鍵參數

  • nms_threshold: 三次非極大值抑制篩選人臉框的IOU閾值,三個網絡可以分別設置,值設置的過小,nms合併的太少,會產生較多的冗餘計算。
  • threshold:人臉框得分閾值,三個網絡可單獨設定閾值,值設置的太小,會有很多框通過,也就增加了計算量,還有可能導致最後不是人臉的框錯認為人臉。
  • mean_vals:三個網絡輸入圖片的均值,需要單獨設置。
  • norm_vals:三個網絡輸入圖片的縮放係數,需要單獨設置。
  • min_size: 最小可檢測圖像,該值大小,可控制圖像金字塔的階層數的參數之一,越小,階層越多,計算越多。本代碼取了40。
  • factor:生成圖像金字塔時候的縮放係數, 範圍(0,1),可控制圖像金字塔的階層數的參數之一,越大,階層越多,計算越多。本文取了0.709。
  • MIN_DET_SIZE:代表PNet的輸入圖像長寬,都為12。

代碼執行流程

生成圖像金字塔

關鍵參數minsize和factor共同決定了圖像金字塔的層數,也就是生成的圖片數量。

這部分的代碼如下:

    // 縮放到12為止  	int MIN_DET_SIZE = 12;  	// 可以檢測的最小人臉  	int minsize = 40;  	float m = (float)MIN_DET_SIZE / minsize;  	minl *= m;  	float factor = 0.709;  	int factor_count = 0;  	vector<float> scales_;  	while (minl>MIN_DET_SIZE) {  		if (factor_count>0)m = m*factor;  		scales_.push_back(m);  		minl *= factor;  		factor_count++;  	}  

這部分代碼中的MIN_DET_SIZE代表縮放的最小尺寸不可以小於12,也就是從原圖縮放到12為止。scales這個vector保存的是每次縮放的係數,它的尺寸代表了可以縮放出的圖片的數量。其中minsize代表可以檢測到的最小人臉大小,這裡設置為40。縮放後的圖片尺寸可以用以下公式計算: ,其中n就是scales的長度,即特徵金字塔層數。

PNet

Pnet只做檢測和回歸任務。在上篇文章中我們知道PNet是要求12*12的輸入的,實際上再訓練的時候是這樣做的。但是測試的時候並不需要把金字塔的每張圖像resize到12乘以12餵給PNet,因為它是全卷積網絡,以直接將resize後的圖像餵給網絡進行Forward。這個時候得到的結果就不是和,而是和。這樣就不用先從resize的圖上截取各種的圖再送入網絡了,而是一次性送入,再根據結果回推每個結果對應的的圖在輸入圖片的什麼位置。然後對於金字塔的每張圖,網絡forward後都會得到屬於人臉的概率以及人臉框回歸的結果。每張圖片會得到個分類得分和個人回歸坐標,然後結合scales可以將每個滑窗映射回原圖,得到真實坐標。

接下來,先根據上面的threshold參數將得分低的區域排除掉,然後執行一遍NMS去除一部分冗餘的重疊框,最後,PNet就得到了一堆人臉框,當然結果還不精細,需要繼續往下走。Pnet的代碼為:

for (size_t i = 0; i < scales_.size(); i++) {  		int hs = (int)ceil(img_h*scales_[i]);  		int ws = (int)ceil(img_w*scales_[i]);  		//ncnn::Mat in = ncnn::Mat::from_pixels_resize(image_data, ncnn::Mat::PIXEL_RGB2BGR, img_w, img_h, ws, hs);  		ncnn::Mat in;  		resize_bilinear(img_, in, ws, hs);  		//in.substract_mean_normalize(mean_vals, norm_vals);  		ncnn::Extractor ex = Pnet.create_extractor();  		ex.set_light_mode(true);  		ex.input("data", in);  		ncnn::Mat score_, location_;  		ex.extract("prob1", score_);  		ex.extract("conv4-2", location_);  		std::vector<Bbox> boundingBox_;  		std::vector<orderScore> bboxScore_;  		generateBbox(score_, location_, boundingBox_, bboxScore_, scales_[i]);  		nms(boundingBox_, bboxScore_, nms_threshold[0]);    		for (vector<Bbox>::iterator it = boundingBox_.begin(); it != boundingBox_.end(); it++) {  			if ((*it).exist) {  				firstBbox_.push_back(*it);  				order.score = (*it).score;  				order.oriOrder = count;  				firstOrderScore_.push_back(order);  				count++;  			}  		}  		bboxScore_.clear();  		boundingBox_.clear();  	}  

其中有2個關鍵的函數,分別是generateBoxnms,我們分別來解析一下,首先看generateBox:

// 根據Pnet的輸出結果,由滑框的得分,篩選可能是人臉的滑框,並記錄該框的位置、人臉坐標信息、得分以及編號  void mtcnn::generateBbox(ncnn::Mat score, ncnn::Mat location, std::vector<Bbox>& boundingBox_, std::vector<orderScore>& bboxScore_, float scale) {  	int stride = 2;  	int cellsize = 12;  	int count = 0;  	//score p 判定為人臉的概率  	float *p = score.channel(1);  	// 人臉框回歸偏移量  	float *plocal = location.channel(0);  	Bbox bbox;  	orderScore order;  	for (int row = 0; row<score.h; row++) {  		for (int col = 0; col<score.w; col++) {  			if (*p>threshold[0]) {  				bbox.score = *p;  				order.score = *p;  				order.oriOrder = count;  				// 對應原圖中的坐標  				bbox.x1 = round((stride*col + 1) / scale);  				bbox.y1 = round((stride*row + 1) / scale);  				bbox.x2 = round((stride*col + 1 + cellsize) / scale);  				bbox.y2 = round((stride*row + 1 + cellsize) / scale);  				bbox.exist = true;  				// 在原圖中的大小  				bbox.area = (bbox.x2 - bbox.x1)*(bbox.y2 - bbox.y1);  				// 當前人臉框的回歸坐標  				for (int channel = 0; channel<4; channel++)  					bbox.regreCoord[channel] = location.channel(channel)[0];  				boundingBox_.push_back(bbox);  				bboxScore_.push_back(order);  				count++;  			}  			p++;  			plocal++;  		}  	}  }  

對於非極大值抑制(NMS),應該先了解一下它的原理。簡單解釋一下就是說:當兩個box空間位置非常接近,就以score更高的那個作為基準,看IOU即重合度如何,如果與其重合度超過閾值,就抑制score更小的box,因為沒有必要輸出兩個接近的box,只保留score大的就可以了。之後我也會盤點各種NMS算法,講講他們的原理,已經在目標檢測學習總結路線中規划上了,請打開公眾號的深度學習欄中的目標檢測路線推文查看我的講解思維導圖。代碼如下,這段代碼以打擂台的生活場景進行注釋,比較好理解:

void mtcnn::nms(std::vector<Bbox> &boundingBox_, std::vector<orderScore> &bboxScore_, const float overlap_threshold, string modelname) {  	if (boundingBox_.empty()) {  		return;  	}  	std::vector<int> heros;  	//sort the score  	sort(bboxScore_.begin(), bboxScore_.end(), cmpScore);    	int order = 0;  	float IOU = 0;  	float maxX = 0;  	float maxY = 0;  	float minX = 0;  	float minY = 0;  	// 規則,站上擂台的擂台主,永遠都是勝利者  	while (bboxScore_.size()>0) {  		order = bboxScore_.back().oriOrder; //取得分最高勇士的編號ID  		bboxScore_.pop_back(); // 勇士出列  		if (order<0)continue; //死的?下一個!(order在(*it).oriOrder = -1;改變)  		if (boundingBox_.at(order).exist == false) continue; //記錄擂台主ID  		heros.push_back(order);  		boundingBox_.at(order).exist = false;//當前這個Bbox為擂台主,簽訂生死簿    		for (int num = 0; num<boundingBox_.size(); num++) {  			if (boundingBox_.at(num).exist) {// 活着的勇士  				//the iou  				maxX = (boundingBox_.at(num).x1>boundingBox_.at(order).x1) ? boundingBox_.at(num).x1 : boundingBox_.at(order).x1;  				maxY = (boundingBox_.at(num).y1>boundingBox_.at(order).y1) ? boundingBox_.at(num).y1 : boundingBox_.at(order).y1;  				minX = (boundingBox_.at(num).x2<boundingBox_.at(order).x2) ? boundingBox_.at(num).x2 : boundingBox_.at(order).x2;  				minY = (boundingBox_.at(num).y2<boundingBox_.at(order).y2) ? boundingBox_.at(num).y2 : boundingBox_.at(order).y2;  				//maxX1 and maxY1 reuse  				maxX = ((minX - maxX + 1)>0) ? (minX - maxX + 1) : 0;  				maxY = ((minY - maxY + 1)>0) ? (minY - maxY + 1) : 0;  				//IOU reuse for the area of two bbox  				IOU = maxX * maxY;  				if (!modelname.compare("Union"))  					IOU = IOU / (boundingBox_.at(num).area + boundingBox_.at(order).area - IOU);  				else if (!modelname.compare("Min")) {  					IOU = IOU / ((boundingBox_.at(num).area<boundingBox_.at(order).area) ? boundingBox_.at(num).area : boundingBox_.at(order).area);  				}  				if (IOU>overlap_threshold) {  					boundingBox_.at(num).exist = false; //如果該對比框與擂台主的IOU夠大,挑戰者勇士戰死  					for (vector<orderScore>::iterator it = bboxScore_.begin(); it != bboxScore_.end(); it++) {  						if ((*it).oriOrder == num) {  							(*it).oriOrder = -1;//勇士戰死標誌  							break;  						}  					}  				}  				//那些距離擂台主比較遠迎戰者幸免於難,將有機會作為擂台主出現  			}  		}  	}  	//從生死簿上剔除,擂台主活下來了  	for (int i = 0; i<heros.size(); i++)  		boundingBox_.at(heros.at(i)).exist = true;  }  

RNet

這以階段就和PNet相比,就需要將圖像resize到(24,24)了。然後剩下的過程也和PNet一樣,做nms。最後還多了一個refineAndSquareBox的後處理過程,這個函數是把所有留下的框變成正方形並且將這些框的邊界限定在原圖長寬範圍內。注意一下,這個階段refineAndSquareBox是在nms之後做的。

//second stage  	count = 0;  	for (vector<Bbox>::iterator it = firstBbox_.begin(); it != firstBbox_.end(); it++) {  		if ((*it).exist) {  			ncnn::Mat tempIm;  			copy_cut_border(img, tempIm, (*it).y1, img_h - (*it).y2, (*it).x1, img_w - (*it).x2);  			ncnn::Mat in;  			resize_bilinear(tempIm, in, 24, 24);  			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 ((score[1])>threshold[1]) {  				for (int channel = 0; channel<4; channel++)  					it->regreCoord[channel] = bbox[channel];  				it->area = (it->x2 - it->x1)*(it->y2 - it->y1);  				it->score = score[1];  				secondBbox_.push_back(*it);  				order.score = it->score;  				order.oriOrder = count++;  				secondBboxScore_.push_back(order);  			}  			else {  				(*it).exist = false;  			}  		}  	}  	printf("secondBbox_.size()=%dn", secondBbox_.size());  	if (count<1)return;  	nms(secondBbox_, secondBboxScore_, nms_threshold[1]);  	refineAndSquareBbox(secondBbox_, img_h, img_w);  

ONet

ONet相比於前面2個階段,多了一個關鍵點回歸的過程。同時需要注意的是這個階段refineAndSquareBox是在nms之前做的。經過這個階段,出來的框就是我們苦苦追尋的人臉框啦,完結。

count = 0;  	for (vector<Bbox>::iterator it = secondBbox_.begin(); it != secondBbox_.end(); it++) {  		if ((*it).exist) {  			ncnn::Mat tempIm;  			copy_cut_border(img, tempIm, (*it).y1, img_h - (*it).y2, (*it).x1, img_w - (*it).x2);  			ncnn::Mat in;  			resize_bilinear(tempIm, in, 48, 48);  			ncnn::Extractor ex = Onet.create_extractor();  			ex.set_light_mode(true);  			ex.input("data", in);  			ncnn::Mat score, bbox, keyPoint;  			ex.extract("prob1", score);  			ex.extract("conv6-2", bbox);  			ex.extract("conv6-3", keyPoint);  			if (score[1]>threshold[2]) {  				for (int channel = 0; channel<4; channel++)  					it->regreCoord[channel] = bbox[channel];  				it->area = (it->x2 - it->x1)*(it->y2 - it->y1);  				it->score = score[1];  				for (int num = 0; num<5; num++) {  					(it->ppoint)[num] = it->x1 + (it->x2 - it->x1)*keyPoint[num];  					(it->ppoint)[num + 5] = it->y1 + (it->y2 - it->y1)*keyPoint[num + 5];  				}    				thirdBbox_.push_back(*it);  				order.score = it->score;  				order.oriOrder = count++;  				thirdBboxScore_.push_back(order);  			}  			else  				(*it).exist = false;  		}  	}    	printf("thirdBbox_.size()=%dn", thirdBbox_.size());  

效果

我們來試試MTCNN算法的檢測效果。

原圖1:

結果圖1:

原圖2(一張有T神的圖片):

結果圖2:

在這裡插入圖片描述

後記

MTCNN的實時性和魯棒性都是相當不錯的,現在相當多公司的檢測任務和識別任務都是借鑒了MTCNN算法,這個算法對於當代的目標檢測任務有重要意義。

參考文章

https://blog.csdn.net/fuwenyan/article/details/77573755