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

前言

上篇講解了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”辏縮放后的圖片尺寸可以用以下公式計算:
minL=orgL*(12/minsize)*factor^n贼陶,其中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é)果就不是1*1*21*1*4趴樱,而是m*m*2m*m*4。這樣就不用先從resize的圖上截取各種12*12*3的圖再送入網(wǎng)絡(luò)了酪捡,而是一次性送入叁征,再根據(jù)結(jié)果回推每個結(jié)果對應(yīng)的12*12的圖在輸入圖片的什么位置。
然后對于金字塔的每張圖逛薇,網(wǎng)絡(luò)forward后都會得到屬于人臉的概率以及人臉框回歸的結(jié)果捺疼。每張圖片會得到m*m*2個分類得分和m*m*4個人回歸坐標(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ù),分別是generateBoxnms卖陵,我們分別來解析一下遭顶,首先看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ù)爷辱,比賽及日常生活等。


圖片.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子淋肾,更是在濱河造成了極大的恐慌,老刑警劉巖弟断,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異趴生,居然都是意外死亡阀趴,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進(jìn)店門冲秽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來舍咖,“玉大人,你說我怎么就攤上這事锉桑∨琶梗” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵民轴,是天一觀的道長攻柠。 經(jīng)常有香客問我,道長后裸,這世上最難降的妖魔是什么瑰钮? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮微驶,結(jié)果婚禮上浪谴,老公的妹妹穿的比我還像新娘开睡。我一直安慰自己,他們只是感情好苟耻,可當(dāng)我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布篇恒。 她就那樣靜靜地躺著,像睡著了一般凶杖。 火紅的嫁衣襯著肌膚如雪胁艰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天智蝠,我揣著相機(jī)與錄音腾么,去河邊找鬼。 笑死杈湾,一個胖子當(dāng)著我的面吹牛解虱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播毛秘,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼饭寺,長吁一口氣:“原來是場噩夢啊……” “哼阻课!你這毒婦竟也來了叫挟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤限煞,失蹤者是張志新(化名)和其女友劉穎抹恳,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體署驻,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡奋献,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了旺上。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瓶蚂。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖宣吱,靈堂內(nèi)的尸體忽然破棺而出窃这,到底是詐尸還是另有隱情,我是刑警寧澤征候,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布杭攻,位于F島的核電站,受9級特大地震影響疤坝,放射性物質(zhì)發(fā)生泄漏兆解。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一跑揉、第九天 我趴在偏房一處隱蔽的房頂上張望锅睛。 院中可真熱鬧,春花似錦、人聲如沸现拒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽具练。三九已至乍构,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扛点,已是汗流浹背哥遮。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留陵究,地道東北人眠饮。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像铜邮,于是被迫代替她去往敵國和親仪召。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容