基於Python的人臉檢測與分類過程詳解

人臉識別

算法簡介

我們的算法可以分成兩個部分,識別人臉位置和確定人臉分類。這兩個部分可以看成:
1.檢測出人臉之間相似性。
2.檢測出人臉之間不同性。
由於這兩項工作截然相反,所以我們使用瞭兩個網絡來分別完成這兩項工作。

人臉檢測

簡述

我們的人臉檢測網絡采用瞭和Faster RCNN類似的策略,但我們在ROI Polling上進行瞭創新,兼顧瞭小目標檢測和大目標檢測,為此,我們還使用瞭改進後的RESNET101_V2的網絡,使我們的網絡對於小目標更加敏感。在增加瞭少量的運算單元後,我們的網絡可以識別24*24像素下的人臉(甚至於更低!)。我們調整瞭網絡結構,並沒有采用傳統的卷積網絡(提取特征)+全連接層(分類)的結構,而是采用瞭全卷積結構,這讓我們的識別網絡的速度遠遠高於傳統的神經網絡識別方法,識別精度也高於傳統的算子和特征值人臉識別算法。

數據集介紹

采用的數據集為FDDB數據集,該數據集圖像+註釋有600M左右。
圖像有各種大小和形狀,主要集中在(300600)*(300600)的像素上。
註:我們的訓練網絡不在乎訓練圖像的大小形狀(隻要長寬大於192就好)。
其註釋內容為圖像中的人臉橢圓框:

[ra, rb, Θ, cx, cy, s]
ra,rb:半長軸、半短軸
cx, cy:橢圓中心點坐標
Θ:長軸與水平軸夾角(頭往左偏Θ為正,頭往右偏Θ為負)
s:置信度得分

通過坐標變換後我們可以得到矩形框:

w = 2*max([abs(ra*math.sin(theta)),abs(rb*math.cos(theta))])
h = 2*max([abs(ra*math.cos(theta)),abs(rb*math.sin(theta))])
rect = [cx-w/2,cy-h/2,w,h]
即:
rect = [x,y,w,h](x,y為左上角坐標)

我們以圖為單位,從圖中抽取128個anchors,這128個anchors包括該圖中的全部正例和隨機的負例。最後使用我們進行坐標變換的矩形框進行Bounding Box回歸。

算法介紹

流程圖

img = tf.constant(img,shape = (1,h,w,mod),dtype = tf.float32) # 圖像原始數據
# 使用無pool1&pool5的RESNET 101
net, endpoints = my_resnet(img,global_pool = False,num_classes=None,is_training=True,reuse = tf.compat.v1.AUTO_REUSE) # net's w&h = original_img's w&h / 8

我們進行模型搭建和使用的平臺為windows10-python3.6.2-tensorflow-gpu。
首先,我們的圖像(img_batch = [batch_size,h,w,mod],batch_size為圖像的數量,h為圖像高度,w為圖像寬度,mod為圖像通道數,這裡我們處理的均為RGB三色彩圖,所以我們的通道數均為3)通過我們改進版的RESNET101_V2網絡,傳統的RESNET101_V2的網絡結構如下:www.biyezuopin.vip

而我們的網絡去掉瞭pool1和pool5層,使網絡放縮系數從32下降到瞭8。這使我們的網絡對於小目標更加的敏感。通過瞭該網絡後,我們得到瞭卷積後的信息圖:img_batch_conv = [batch_size,h/8,w/8,2048]

weights = {
            'down':tf.compat.v1.get_variable(name = 'w_down',shape = [1,1,2048,1024]),# 降采樣
            'feature':tf.compat.v1.get_variable(name = 'w_feature',shape = [1,1,1024,K*K*2])
            }
biases = {
            'down':tf.compat.v1.get_variable(name = 'b_down',shape = [1024,]), # 降采樣
            'feature':tf.compat.v1.get_variable(name = 'b_feature',shape = [K*K*2,])
        }

img_batch_conv首先通過一個shape = [1,1,2048,1024]的卷積算子,該算子的作用是進一步的特征提取和降采樣。我們采用多步特征提取的策略是由於在RCNN[1]一文中,作者提出在VOC2012測試集下,三層的特征識別網絡比一層特征識別網絡的正確率要高。

通過該算子我們得到瞭一個[batch_size,h/8,w/8,1024]結構的數據,將該數據通過一個[1,1,1024,K*K*C]的算子得到特征圖feature_map。feature_map的概念在Faster RCNN[2]的文章內提出,提取特征圖的算子的K*K代表著每一塊feature_map有K*K個bin,而C代表著識別物體的類別+1(背景),這裡我們的K取3,C取2(隻有兩個分類:人臉和背景)。

    with tf.compat.v1.variable_scope('RPN', reuse=tf.compat.v1.AUTO_REUSE):
        weights = {
            'rpn_1':tf.compat.v1.get_variable(name = 'w_rpn_1_1',shape = [3,3,1024,1]), # 高:寬 1:1的卷積
            'rpn_2':tf.compat.v1.get_variable(name = 'w_rpn_1_2',shape = [3,6,1024,1]), # 高:寬 1:2的卷積
            'rpn_3':tf.compat.v1.get_variable(name = 'w_rpn_2_1',shape = [6,3,1024,1]), # 高:寬 2:1的卷積
            'rpn_4':tf.compat.v1.get_variable(name = 'w_rpn_2_2',shape = [6,6,1024,1]),
            'rpn_5':tf.compat.v1.get_variable(name = 'w_rpn_2_4',shape = [6,12,1024,1]),
            'rpn_6':tf.compat.v1.get_variable(name = 'w_rpn_4_2',shape = [12,6,1024,1]),
            'rpn_7':tf.compat.v1.get_variable(name = 'w_rpn_4_4',shape = [12,12,1024,1]),
            'rpn_8':tf.compat.v1.get_variable(name = 'w_rpn_4_8',shape = [12,24,1024,1]),
            'rpn_9':tf.compat.v1.get_variable(name = 'w_rpn_8_4',shape = [24,12,1024,1])
        }
        biases = {
            'rpn_1':tf.compat.v1.get_variable(name = 'b_rpn_1_1',shape = [1,]),
            'rpn_2':tf.compat.v1.get_variable(name = 'b_rpn_1_2',shape = [1,]),
            'rpn_3':tf.compat.v1.get_variable(name = 'b_rpn_2_1',shape = [1,]),
            'rpn_4':tf.compat.v1.get_variable(name = 'b_rpn_2_2',shape = [1,]),
            'rpn_5':tf.compat.v1.get_variable(name = 'b_rpn_2_4',shape = [1,]),
            'rpn_6':tf.compat.v1.get_variable(name = 'b_rpn_4_2',shape = [1,]),
            'rpn_7':tf.compat.v1.get_variable(name = 'b_rpn_4_4',shape = [1,]),
            'rpn_8':tf.compat.v1.get_variable(name = 'b_rpn_4_8',shape = [1,]),
            'rpn_9':tf.compat.v1.get_variable(name = 'b_rpn_8_4',shape = [1,])
        }

我們將得到的feature_map = [batch_size,h/8,w/8,K*K*C]使用三種不同形狀,三種不同大小,一共九種不同形狀或大小的卷積核對我們的網絡進行卷積,得到瞭9種不同形狀或大小的archors中是否存在人臉的概率。這裡我們雖然沿用瞭Faster RCNN[2]中anchor的概念,但我們並沒有使用ROI Pooling而是隻使用瞭ROI。因為Tensorflow采用的是流圖計算,增加ROI Pooling反而會讓每個anchor獨立計算,大大增加瞭我們的計算量,而且不同大小的anchor進行Pooling後均會生成K*K形狀的數據,不方便我們的網絡對於不同大小的anchor進行不同狀態的識別。而且由於我們隻用分成背景和人臉兩個類別,所以即使不進行ROI Pooling,我們網絡所需的運算單元也不會增加太多。所以我們采用瞭9種不同形狀的卷積核對應九種anchor的策略。通過RPN評價層後,我們可以得到對於每個區域是否存在人臉的評價。

這裡我們訓練采用瞭YOLO[3]而不是Faster RCNN[2]的訓練策略,即我們對於不同比例的正例和負例采用比例因子去平衡它。這是因為我們是一張一張圖去訓練的,訓練的圖中正例的數量遠小於負例。

loss_rpn[i] = -(up*tf.math.log(pred_rpn[i])*rpn_view[i] + (1 - rpn_view[i])*tf.math.log(1 - pred_rpn[i]))

這裡loss_rpn[i]為第i個anchor的loss,up為正例激勵因子,pred_rpn[i]為網絡預測的第i個anchor的結果,rp_view[i]為第i個anchor的真實結果。我們的損失函數使用的是交叉熵。

之後,我們將選出來的區域的feature_map,通過ROI Pooling。在Bounding Box回歸之前通過ROI Pooling一方面是由於Bounding Box回歸隻針對正例(選出來的區域),區域的個數較少,即使創建獨立運算的anchor數量也不多,訓練壓力不大;二是對於Bounding Box回歸,anchor的大小和形狀不具有太大的意義。Bounding Box回歸的計算規則如下:www.biyezuopin.vip

dx = x - x'
dy = y - y'
kw = w / w'
kh = h / h'
# 計算損失
loss_bbox = (pre_bbox - select_bbox) * data_type 
loss_bbox = tf.reduce_mean(loss_bbox*loss_bbox)

x,y為中心點坐標,w,h為寬高。
通過網絡我們可以得到[dx,dy,dw,dh]
loss_bbox即Bounding Box回歸的loss,損失函數我們采用的是平方函數。

我們首先通過RPN評價層得到評分最高的N個anchor,每個anchor都帶有[ax,ay,aw,ah]的屬性,ax,ay為網絡放縮下的左上角坐標,aw,ah為網絡放縮下的寬高。所以首先要對放縮後的圖像區域進行恢復:

x' = ax * NET_SCALE
y' = ay * NET_SCALE
w' = aw * NET_SCALE
h' = ah * NET_SCALE

這裡我們的網絡放縮系數NET_SACLE為8。

然後進行Bounding Box回歸:

x = x' + dx
y = y' + dy
w = w' * kw
h = h' * kh

得到[x,y,w,h],如果該區域與其他比它得分高的區域的IOU>0.5的情況下,該區域會被抑制(NMS非極大值抑制)。

測試網絡

這裡我們給出測試網絡的初始化部分:

def loadModel(self,model_path = RPN_BATCH_PATH):
        """
        從model_path中加載模型
        """
        with tf.compat.v1.variable_scope('RPN', reuse=tf.compat.v1.AUTO_REUSE):
            weights = {
                'rpn_1':tf.compat.v1.get_variable(name = 'w_rpn_1_1',shape = [3,3,K*K*2,1]), # 高:寬 1:1的卷積
                'rpn_2':tf.compat.v1.get_variable(name = 'w_rpn_1_2',shape = [3,6,K*K*2,1]), # 高:寬 1:2的卷積
                'rpn_3':tf.compat.v1.get_variable(name = 'w_rpn_2_1',shape = [6,3,K*K*2,1]), # 高:寬 2:1的卷積
                'rpn_4':tf.compat.v1.get_variable(name = 'w_rpn_2_2',shape = [6,6,K*K*2,1]),
                'rpn_5':tf.compat.v1.get_variable(name = 'w_rpn_2_4',shape = [6,12,K*K*2,1]),
                'rpn_6':tf.compat.v1.get_variable(name = 'w_rpn_4_2',shape = [12,6,K*K*2,1]),
                'rpn_7':tf.compat.v1.get_variable(name = 'w_rpn_4_4',shape = [12,12,K*K*2,1]),
                'rpn_8':tf.compat.v1.get_variable(name = 'w_rpn_4_8',shape = [12,24,K*K*2,1]),
                'rpn_9':tf.compat.v1.get_variable(name = 'w_rpn_8_4',shape = [24,12,K*K*2,1])
            }
            biases = {
                'rpn_1':tf.compat.v1.get_variable(name = 'b_rpn_1_1',shape = [1,]),
                'rpn_2':tf.compat.v1.get_variable(name = 'b_rpn_1_2',shape = [1,]),
                'rpn_3':tf.compat.v1.get_variable(name = 'b_rpn_2_1',shape = [1,]),
                'rpn_4':tf.compat.v1.get_variable(name = 'b_rpn_2_2',shape = [1,]),
                'rpn_5':tf.compat.v1.get_variable(name = 'b_rpn_2_4',shape = [1,]),
                'rpn_6':tf.compat.v1.get_variable(name = 'b_rpn_4_2',shape = [1,]),
                'rpn_7':tf.compat.v1.get_variable(name = 'b_rpn_4_4',shape = [1,]),
                'rpn_8':tf.compat.v1.get_variable(name = 'b_rpn_4_8',shape = [1,]),
                'rpn_9':tf.compat.v1.get_variable(name = 'b_rpn_8_4',shape = [1,])
            }
        with tf.compat.v1.variable_scope('BBOX', reuse=tf.compat.v1.AUTO_REUSE):
            weights['bbox'] = tf.compat.v1.get_variable(name = 'w_bbox',shape = [K,K,K*K*2,4]) # 分類
            biases['bbox'] = tf.compat.v1.get_variable(name = 'b_bbox',shape = [4,]) # 分類
        weights['down'] = tf.compat.v1.get_variable(name = 'w_down',shape = [1,1,2048,1024])# 降采樣
        weights['feature'] = tf.compat.v1.get_variable(name = 'w_feature',shape = [1,1,1024,K*K*2])
        biases['down'] = tf.compat.v1.get_variable(name = 'b_down',shape = [1024,]) # 降采樣
        biases['feature'] = tf.compat.v1.get_variable(name = 'b_feature',shape = [K*K*2,])
        self.img = tf.compat.v1.placeholder(dtype = tf.float32,shape = (1,self.h,self.w,3))
        # 使用無pool1&pool5的RESNET 101
        net, endpoints = my_resnet(self.img,global_pool = False,num_classes=None,is_training=True,reuse = tf.compat.v1.AUTO_REUSE) # net's w&h = original_img's w&h / 16
        net = tf.nn.conv2d(input = net,filter = weights['down'],strides = [1, 1, 1, 1],padding = 'VALID')
        net = tf.add(net,biases['down'])
        # 生成feature_map
        self.feature_map = tf.nn.conv2d(input = net,filter = weights['feature'],strides = [1, 1, 1, 1],padding = 'VALID')
        self.feature_map = tf.add(self.feature_map,biases['feature'])
        self.pred_rpn = [None]*9
        for i in range(9):
            r = tf.nn.conv2d(input = self.feature_map,filter = weights['rpn_' + str(i+1)],strides = [1, 1, 1, 1],padding = 'VALID')
            r = tf.reshape(r,r.get_shape().as_list()[1:-1])
            self.pred_rpn[i] = tf.add(r,biases['rpn_' + str(i+1)])
            self.pred_rpn[i] = tf.sigmoid(self.pred_rpn[i])
        self.select = tf.compat.v1.placeholder(dtype = tf.float32,shape = (self.RPN_RESULT_NUM,K,K,K*K*2))
        self.pre_bbox = tf.nn.conv2d(self.select,weights['bbox'],[1,1,1,1],padding = 'VALID')
        self.pre_bbox = tf.add(self.pre_bbox,biases['bbox'])
        self.pre_bbox = tf.reshape(self.pre_bbox,shape = (self.RPN_RESULT_NUM,4))
        saver = tf.compat.v1.train.Saver(tf.compat.v1.get_collection(tf.compat.v1.GraphKeys.TRAINABLE_VARIABLES))
        self.sess =  tf.compat.v1.Session()
        init = tf.compat.v1.global_variables_initializer()
        self.sess.run(init)
        saver.restore(self.sess,RPN_BATCH_PATH)

結果預覽

我們從測試集隨機取出兩張圖片進行測試
我們在測試時,需要把圖像resize到合適的大小,這裡選擇的是192*384,得益於我們改進後的RESNET101_V2,我們的最小寬度和長度是普通網絡的1/8,可以適配於我們測試集,也能適配於大多數情況。

第一張圖片:
RPN結果:

經過Bounding Box回歸後

我們選取瞭一張圖中的TOP5Answer,即得分最高的5個anchors,如RPN結果。
之後采取瞭Bounding Box回歸,得到瞭最終結果,如第二張圖所示。
我們可以看到RPN選取的anchors隻包括瞭頭像中的中間部分,經過Bounding Box回歸之後,選取框完好的罩住瞭頭像。

RPN結果:

經過Bounding Box回歸後

同樣,RPN選取的anchors與真實框有偏移,而Bounding Box回歸修補瞭偏移量。

我們上面測試時采用Top5Answer是由於我們的網絡是在個人電腦上訓練的,訓練次數有限且訓練時長也有限,所以訓練出來的模型效果還不能達到能完全識別出人臉,所以Top5Answer的機制可以顯著提高識別機率,當然也會帶來額外的計算量。

運行速度:

這裡我們的速度在2.6s一張圖左右,這也是由於我們使用的個人電腦性能不足的原因,也是由於我們在結果測試時同時進行瞭繪圖和分析結果所帶來的額外計算量。

到此這篇關於基於Python的人臉檢測與分類的文章就介紹到這瞭,更多相關python人臉檢測內容請搜索LevelAH以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持LevelAH!

推薦閱讀: