超詳細(xì)的人臉檢測:MTCNN代碼分析,手把手帶你從零開始碼代碼

前言

之前我發(fā)過MTCNN的原理分析,不了解的朋友可以看看深刻理解MTCNN原理, 超級詳細(xì),從零開始做人臉檢測。Tensorflow2實現(xiàn)
授人以魚,不如授人以漁,本文與其他代碼分析的文章不一樣,本文不會涉及原理過多的東西,原理參照我的上一片文章,主要以代碼段的思路講解,從最開始得到預(yù)訓(xùn)練權(quán)重后,怎么解析參數(shù),怎么一步一步構(gòu)建代碼框架。可以執(zhí)行獲得最后結(jié)果,優(yōu)化部分留給讀者自行鉆研!本文盡量不出現(xiàn)大段代碼。基于Tensorflow2.0/keras。講的主要是思路,別的框架讀者一樣可以寫出來
我的參考代碼https://github.com/Luo-DH/MTCNN_test歡迎給我個star,如果你喜歡的話,歡迎給我點個贊,加個關(guān)注!!!


準(zhǔn)備

  • 我們需要pnet.h5, rnet.h5, onet.h5,我的github里面可以拿到這三個網(wǎng)絡(luò)的預(yù)訓(xùn)練權(quán)重
  • 還需要一個能打代碼的東西,可以是記事本,可以是vim,可以是pycharm,推薦使用jupyter notebook,方便看到每一步的結(jié)果。
  • 熱情,毅力

開始

思路

  1. 環(huán)境搭建
  2. 整體流程

環(huán)境搭建

推薦大家使用虛擬環(huán)境,如果你是小白不了解虛擬環(huán)境,可以安裝anaconda。我以前用的anaconda,后來用上virtualenvwrapper就再也不想用anaconda了。害!??!

  1. 搭建環(huán)境LINUX
# 新建一個文件夾
mkdir mtcnn_test
# 進(jìn)入文件夾目錄
cd mtcnn_test
# 查看文件夾的內(nèi)容
ll # 是個空文件夾
# 創(chuàng)建虛擬環(huán)境
mkvirtualenv ENV_TF2
# 安裝相關(guān)的包和模塊
pip install tensorflow
pip install opencv-python
pip install numpy
pip install notebook

使用windows或者pycharm類似,只需要將相關(guān)的包和模塊安裝上就行.

  1. 整體流程
  • 獲取圖片
  • 傳入pnet網(wǎng)絡(luò)
  • 將pnet網(wǎng)絡(luò)獲得的輸出傳入rnet網(wǎng)絡(luò)
  • 將rnet網(wǎng)絡(luò)獲得的輸出傳入onet網(wǎng)絡(luò)
  • 得到最后輸出的矩形框

獲取圖片
import cv2
image = cv2.imread("./face.jpg")# 讀取的圖片最好是正方形,
                                # 雖然mtcnn支持不同尺寸的圖片輸入,我們開始
                                # 使用正方形比較直觀

推薦使用opencv讀取圖片。主要是我個人用的習(xí)慣而已啦!當(dāng)然可以使用其他方式讀取圖片。需要注意的是opencv讀取的圖片顏色通道是BGR,而我們需要獲得RGB順序通道的圖片.

# 如果是用opencv讀取圖片,一定要走這一步
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
圖像金字塔

論文中提到將圖像進(jìn)行不同比例的縮放,以便于獲得不同大小的人臉,具體原因可以看我之前的博客,有詳細(xì)說明。

factor = 0.709 比例因子,一般用這個比例進(jìn)行縮放

  • 獲得縮放比例
factor = 0.709
image_width = image.shape[0]
scales = [(factor**i) for i in range(0, 10) if (factor**i)*image_width > 12]
  • 將圖片進(jìn)行縮放
#創(chuàng)建一個列表,用于存放pnet輸入所需要的圖片
imgs = []
for scale in scales:
    new_width = int(image.shape[0] * scale) # 必須是整數(shù)才能作為新圖像的邊長
    new_height = int(image.shape[1] * scale)
    # 進(jìn)行縮放
    img_ =  cv2.resize(image.copy(), (new_width, new_height))
    
    imgs.append(img_)
    
圖片預(yù)處理
pnet_need_imgs = []
for img in imgs:
    img = (img - 127.5)/127.5 # 歸一化
    # 歸一化的具體原因此處不展開
    # 修改圖片的維數(shù)為四維(網(wǎng)絡(luò)的輸入必須是思維)
    img = img.reshape(1, *img.shape)
    # shape=(1, x, x, 3)
    pnet_need_imgs.append(img)# 獲得pnet的輸入

目前為止我們已經(jīng)得到了pnet的輸入,另外一種獲得方法可以參考我github上的代碼,效率會更高。之后我們來構(gòu)建pnet網(wǎng)絡(luò)的結(jié)構(gòu)和讀取預(yù)訓(xùn)練權(quán)重

pnet網(wǎng)絡(luò)初始化

這一部分不需要理解,就只是按照論文的網(wǎng)絡(luò)架構(gòu)讀取,讀者可以直接復(fù)制

import tensorflow as tf
print("Tensorflow Version: {}".format(tf.__version__)) # 2.0以上

    def create_model():
        """定義PNet網(wǎng)絡(luò)的架構(gòu)"""
        input = tf.keras.Input(shape=[None, None, 3])
        x = tf.keras.layers.Conv2D(10, (3, 3), strides=1,padding='valid', name='conv1')(input)
        x = tf.keras.layers.PReLU(shared_axes=[1, 2],name='PReLU1')(x)
        x = tf.keras.layers.MaxPooling2D()(x)
        x = tf.keras.layers.Conv2D(16, (3, 3), strides=1,padding='valid',name='conv2')(x)
        x = tf.keras.layers.PReLU(shared_axes=[1, 2],name='PReLU2')(x)
        x = tf.keras.layers.Conv2D(32, (3, 3),strides=1, padding='valid', name='conv3')(x)
        x = tf.keras.layers.PReLU(shared_axes=[1, 2],name='PReLU3')(x)

        classifier = tf.keras.layers.Conv2D(2, (1, 1),activation='softmax',name='conv4-1')(x)
        bbox_regress = tf.keras.layers.Conv2D(4, (1, 1),name='conv4-2')(x)

        model = tf.keras.models.Model([input], [classifier, bbox_regress])

        return model
    
    # 創(chuàng)建模型
    model = create_model()
    #model.summary() 可以看到網(wǎng)絡(luò)的架構(gòu)
    model.load_weights("./pnet.h5", by_names=True)# 讀取權(quán)重,輸入pnet.h5的地址

模型已經(jīng)處理完畢,只需要把圖片一張一張輸進(jìn)去就可以了

輸入網(wǎng)絡(luò)預(yù)測
outs = [] # 用來存放輸出的結(jié)果
for img in pnet_need_imgs:
    # 一張一張圖片輸入
    out = model.predict(img)
    
    outs.append(out)

此時已經(jīng)獲得了pnet網(wǎng)絡(luò)的輸出結(jié)果,結(jié)果的具體含義參考我之前的博客,此處依舊不展開。下一步是解析這些輸出結(jié)果

結(jié)果解析

out的內(nèi)容包括了人臉的置信度還有對應(yīng)的偏移量的值,我們只需要拿到置信度大于閾值的那個out,然后拿到這個out的偏移量,將偏移量加上坐標(biāo)的值,就可以獲得預(yù)測的人臉框

我用一個out舉例子,上面得到的outs,只需要做個循環(huán)就可以得到結(jié)果

先獲得人臉置信度
cls_prob = out[0][:, :, 1]
(x, y) = np.where(cls_prob > 0.5) # 假設(shè)閾值是0.5
scores= np.array(out[0][i, x, y, 1][np.newaxis, :].T) # 把閾值拿出來,后面需要用到
# 此時我們已經(jīng)獲得了大于閾值的位置
# 我們需要把對應(yīng)位置的偏移量拿出來
offset = out[1][i, x, y]*12*(1/scale) # scale是這張圖片對應(yīng)的縮放比例,我要要把圖像
                                                                      # 還原成原來的比例,所以需要乘以(1/scale)
獲得對應(yīng)的矩形框的坐標(biāo)
# 獲得矩形框的坐標(biāo)
bbx = np.array((y, x)).T
# 左上角的坐標(biāo)
left_top = np.fix(((bbx * 2) + 0) * (1/scale)) 
# 右下角的坐標(biāo)
right_down = np.fix(((bbx * 2) + 11) * (1/scale))
# 獲得矩形框的坐標(biāo) [x1, y1, x2, y2]
boundingbox = np.concatenate((left_top, right_down), axis=1)
將矩形框的坐標(biāo)與偏移量的坐標(biāo)相加
#將矩形框和偏移量相加
boundingbox = boundingbox + offset

此時我們得到的矩形框坐標(biāo)[x1,y1,x2,y2]就是解析過后的值,可是這個值仍然可能出現(xiàn)問題,比如出現(xiàn)負(fù)數(shù)或者x2>x1的情況,所以我們還需要簡單處理一下

其他處理
# 把矩形框和得分放在一起,方便后面作非極大值抑制
boundingbox = np.concatenate((boundingbox, scores), axis=1)

處理就是將值簡單的微調(diào),都可以處理成函數(shù),具體的函數(shù)可以參考我的代碼,主要在src/Net.py中,主要是_nms(), _rect2square(), _trimming_frame()這三那個函數(shù),就不放上來了,可以直接調(diào)用

rects = _rect2square(boundingbox)
rects = _trimming_frame(rects)
rects = _nms(rects, 0.7)
pnet總結(jié)

此時我們已經(jīng)完全獲取了pnet處理后得到的矩形框,讀者可以用以下函數(shù)來查看所獲得的矩形框

for rect in rects:
    img_ = cv2.rectangle(image.copy(), (int(rect[0]), int(rect[1])), (int(rect[0]), int(rect[1])), (0, 255, 0), 4)
    
    cv2.imshow("img_", img_)
    cv2.waitKey(0)
    cv2.destroyAllWIndows()
rnet部分
處理獲得rnet輸入所需要的圖片

把pnet得到的矩形框在原圖中截取出來

rnet_need_imgs = []
for rect in rects:

    tmp_roi = image.copy()[int(rect[1]): int(rect[3]), \
                           int(rect[0]): int(rect[2])]

    # resize成24x24大小
    tmp_roi = cv2.resize(tmp_roi, (24, 24))

    rnet_need_imgs.append(tmp_roi)

處理rnet網(wǎng)絡(luò)結(jié)構(gòu)

以下代碼一樣讀者可以直接復(fù)制,基本沒有可以修改的地方

def create_model(cls):
    """定義RNet網(wǎng)絡(luò)的架構(gòu)"""
    input = tf.keras.Input(shape=[24, 24, 3])
    x = tf.keras.layers.Conv2D(28, (3, 3), strides=1, padding='valid', name='conv1')(input)
    x = tf.keras.layers.PReLU(shared_axes=[1, 2], name='prelu1')(x)
    x = tf.keras.layers.MaxPooling2D(pool_size=3, strides=2, padding='same')(x)

    x = tf.keras.layers.Conv2D(48, (3, 3), strides=1, padding='valid', name='conv2')(x)
    x = tf.keras.layers.PReLU(shared_axes=[1, 2], name='prelu2')(x)
    x = tf.keras.layers.MaxPooling2D(pool_size=3, strides=2)(x)

    x = tf.keras.layers.Conv2D(64, (2, 2), strides=1, padding='valid', name='conv3')(x)
    x = tf.keras.layers.PReLU(shared_axes=[1, 2], name='prelu3')(x)

    x = tf.keras.layers.Permute((3, 2, 1))(x)
    x = tf.keras.layers.Flatten()(x)

    x = tf.keras.layers.Dense(128, name='conv4')(x)
    x = tf.keras.layers.PReLU(name='prelu4')(x)

    classifier = tf.keras.layers.Dense(2, activation='softmax', name='conv5-1')(x)
    bbox_regress = tf.keras.layers.Dense(4, name='conv5-2')(x)

    model = tf.keras.models.Model([input], [classifier, bbox_regress])

    return model

model = create_model()

model.load_weights("./rnet.h5", by_names=True)
輸入網(wǎng)絡(luò)進(jìn)行預(yù)測
out = model.predict(rnet_need_imgs)

rnet不需要循環(huán)放入網(wǎng)絡(luò),圖片被一次傳入了網(wǎng)絡(luò),得到了所有的結(jié)果

處理結(jié)果
classifier = out[0]
x = np.where(classifier[:, 1] > 0.6) # 閾值設(shè)定為0.6
獲取boundingbox的值有點復(fù)雜
# 獲得相應(yīng)位置的offset值
offset = out[1]
offset = offset[x, None]

dx1 = np.array(offset[0])[:, :, 0]
dy1 = np.array(offset[0])[:, :, 1]
dx2 = np.array(offset[0])[:, :, 2]
dy2 = np.array(offset[0])[:, :, 3]

# 我們需要用到pnet獲得的矩形框的值
pnet_got_rects = np.array(rects)

通過pnet的rects,我們可以拿到對應(yīng)的寬和高,用于還原圖大小

x1 = np.array(pnet_got_rects[x][:, 0])[np.newaxis, :].T
y1 = np.array(pnet_got_rects[x][:, 1])[np.newaxis, :].T
x2 = np.array(pnet_got_rects[x][:, 2])[np.newaxis, :].T
y2 = np.array(pnet_got_rects[x][:, 3])[np.newaxis, :].T

w = x2 - x1
h = y2 - y1

new_x1 = np.fix(x1 + dx1*w)
new_x2 = np.fix(x2 + dx2*w)
new_y1 = np.fix(y1 + dy1*h)
new_y2 = np.fix(y2 + dy2*h)
score = np.array(classifier[x, 1]).T

boundingbox = np.concatenate((new_x1, 
                              new_y1, 
                              new_x2, 
                              new_y2, 
                              score), axis=1)

此時,我們已經(jīng)得到了rnet的輸出結(jié)果,我們需和pnet相同的處理

rects = _rect2square(boundingbox)
rects = _trimming_frame(rects)
rects = _nms(rects, 0.7)
rnet總結(jié)

我們已經(jīng)獲得了rnet的處理結(jié)果,和上面方法一樣,可以畫出圖片查看以下

onet部分

留給讀者思考,處理方式和rnet差不多,有疑問可以和我聯(lián)系!

結(jié)語

代碼可能有很多不清晰的地方,建議讀者多看幾次,特別是關(guān)于mtcnn原理的解讀,更加方便看懂此篇文章。如果有疑問歡迎與我聯(lián)系,如果對你有幫助,可以給我點個贊,加個關(guān)注,謝謝!


參考代碼https://github.com/Luo-DH/MTCNN_test

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容