前言
上篇講解了MTCNN算法的算法原理以及訓(xùn)練細(xì)節(jié),這篇文章主要從源碼實現(xiàn)的角度來解析一下MTCNN算法缰犁。我要解析的代碼來自github的https://github.com/ElegantGod/ncnn中的mtcnn.cpp料祠。
網(wǎng)絡(luò)結(jié)構(gòu)
再貼一下MTCNN的網(wǎng)絡(luò)結(jié)構(gòu),方便注釋代碼的時候可以隨時查看。
MTCNN代碼運(yùn)行流程
代碼中的關(guān)鍵參數(shù)
- nms_threshold: 三次非極大值抑制篩選人臉框的IOU閾值键俱,三個網(wǎng)絡(luò)可以分別設(shè)置王凑,值設(shè)置的過小搪柑,nms合并的太少,會產(chǎn)生較多的冗余計算索烹。
- threshold:人臉框得分閾值工碾,三個網(wǎng)絡(luò)可單獨設(shè)定閾值,值設(shè)置的太小百姓,會有很多框通過渊额,也就增加了計算量,還有可能導(dǎo)致最后不是人臉的框錯認(rèn)為人臉垒拢。
- mean_vals:三個網(wǎng)絡(luò)輸入圖片的均值旬迹,需要單獨設(shè)置。
- norm_vals:三個網(wǎng)絡(luò)輸入圖片的縮放系數(shù)求类,需要單獨設(shè)置舱权。
- min_size: 最小可檢測圖像,該值大小仑嗅,可控制圖像金字塔的階層數(shù)的參數(shù)之一宴倍,越小,階層越多仓技,計算越多鸵贬。本代碼取了40。
- factor:生成圖像金字塔時候的縮放系數(shù), 范圍(0,1)脖捻,可控制圖像金字塔的階層數(shù)的參數(shù)之一阔逼,越大,階層越多地沮,計算越多嗜浮。本文取了0.709羡亩。
- MIN_DET_SIZE:代表PNet的輸入圖像長寬,都為12危融。
代碼執(zhí)行流程
生成圖像金字塔
關(guān)鍵參數(shù)minsize和factor共同決定了圖像金字塔的層數(shù)畏铆,也就是生成的圖片數(shù)量。
這部分的代碼如下:
// 縮放到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
保存的是每次縮放的系數(shù),它的尺寸代表了可以縮放出的圖片的數(shù)量蛋勺。其中minsize
代表可以檢測到的最小人臉大小瓦灶,這里設(shè)置為40”辏縮放后的圖片尺寸可以用以下公式計算:
贼陶,其中n就是
scales
的長度,即特征金字塔層數(shù)巧娱。
PNet
Pnet只做檢測和回歸任務(wù)碉怔。在上篇文章中我們知道PNet是要求12*12的輸入的,實際上再訓(xùn)練的時候是這樣做的家卖。但是測試的時候并不需要把金字塔的每張圖像resize到12乘以12喂給PNet眨层,因為它是全卷積網(wǎng)絡(luò),以直接將resize后的圖像喂給網(wǎng)絡(luò)進(jìn)行Forward上荡。這個時候得到的結(jié)果就不是和
趴樱,而是
和
。這樣就不用先從resize的圖上截取各種
的圖再送入網(wǎng)絡(luò)了酪捡,而是一次性送入叁征,再根據(jù)結(jié)果回推每個結(jié)果對應(yīng)的
的圖在輸入圖片的什么位置。
然后對于金字塔的每張圖逛薇,網(wǎng)絡(luò)forward后都會得到屬于人臉的概率以及人臉框回歸的結(jié)果捺疼。每張圖片會得到個分類得分和
個人回歸坐標(biāo),然后結(jié)合
scales
可以將每個滑窗映射回原圖永罚,得到真實坐標(biāo)啤呼。
接下來,先根據(jù)上面的threshold
參數(shù)將得分低的區(qū)域排除掉呢袱,然后執(zhí)行一遍NMS去除一部分冗余的重疊框官扣,最后,PNet就得到了一堆人臉框羞福,當(dāng)然結(jié)果還不精細(xì)惕蹄,需要繼續(xù)往下走。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個關(guān)鍵的函數(shù),分別是generateBox
和nms
卖陵,我們分別來解析一下遭顶,首先看generateBox
:
// 根據(jù)Pnet的輸出結(jié)果,由滑框的得分泪蔫,篩選可能是人臉的滑框棒旗,并記錄該框的位置、人臉坐標(biāo)信息鸥滨、得分以及編號
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;
// 對應(yīng)原圖中的坐標(biāo)
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);
// 當(dāng)前人臉框的回歸坐標(biāo)
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)嗦哆,應(yīng)該先了解一下它的原理谤祖。簡單解釋一下就是說:當(dāng)兩個box空間位置非常接近婿滓,就以score更高的那個作為基準(zhǔn),看IOU即重合度如何粥喜,如果與其重合度超過閾值凸主,就抑制score更小的box,因為沒有必要輸出兩個接近的box额湘,只保留score大的就可以了卿吐。之后我也會盤點各種NMS算法,講講他們的原理锋华,已經(jīng)在目標(biāo)檢測學(xué)習(xí)總結(jié)路線中規(guī)劃上了嗡官,請打開公眾號的深度學(xué)習(xí)欄中的目標(biāo)檢測路線推文查看我的講解思維導(dǎo)圖。代碼如下毯焕,這段代碼以打擂臺的生活場景進(jìn)行注釋衍腥,比較好理解:
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;
// 規(guī)則,站上擂臺的擂臺主纳猫,永遠(yuǎn)都是勝利者
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;//當(dāng)前這個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夠大尚骄,挑戰(zhàn)者勇士戰(zhàn)死
for (vector<orderScore>::iterator it = bboxScore_.begin(); it != bboxScore_.end(); it++) {
if ((*it).oriOrder == num) {
(*it).oriOrder = -1;//勇士戰(zhàn)死標(biāo)志
break;
}
}
}
//那些距離擂臺主比較遠(yuǎn)迎戰(zhàn)者幸免于難,將有機(jī)會作為擂臺主出現(xiàn)
}
}
}
//從生死簿上剔除侵续,擂臺主活下來了
for (int i = 0; i<heros.size(); i++)
boundingBox_.at(heros.at(i)).exist = true;
}
RNet
這以階段就和PNet相比倔丈,就需要將圖像resize到(24,24)了。然后剩下的過程也和PNet一樣状蜗,做nms需五。最后還多了一個refineAndSquareBox
的后處理過程,這個函數(shù)是把所有留下的框變成正方形并且將這些框的邊界限定在原圖長寬范圍內(nèi)诗舰。注意一下警儒,這個階段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()=%d\n", secondBbox_.size());
if (count<1)return;
nms(secondBbox_, secondBboxScore_, nms_threshold[1]);
refineAndSquareBbox(secondBbox_, img_h, img_w);
ONet
ONet相比于前面2個階段,多了一個關(guān)鍵點回歸的過程蜀铲。同時需要注意的是這個階段refineAndSquareBox
是在nms
之前做的边琉。經(jīng)過這個階段,出來的框就是我們苦苦追尋的人臉框啦记劝,完結(jié)变姨。
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()=%d\n", thirdBbox_.size());
效果
我們來試試MTCNN算法的檢測效果。
原圖1:
結(jié)果圖1:
原圖2(一張有T神的圖片):
結(jié)果圖2:
后記
MTCNN的實時性和魯棒性都是相當(dāng)不錯的厌丑,現(xiàn)在相當(dāng)多公司的檢測任務(wù)和識別任務(wù)都是借鑒了MTCNN算法定欧,這個算法對于當(dāng)代的目標(biāo)檢測任務(wù)有重要意義。
參考文章
https://blog.csdn.net/fuwenyan/article/details/77573755
歡迎關(guān)注我的微信公眾號GiantPadaCV怒竿,期待和你一起交流機(jī)器學(xué)習(xí)砍鸠,深度學(xué)習(xí),圖像算法耕驰,優(yōu)化技術(shù)爷辱,比賽及日常生活等。