最近看了一些人臉識別的綜述及幾篇經典論文。這里簡單記錄下MTCNN論文及Tensorflow的復現過程。感覺人臉檢測屬於目標檢測下的一個方向,不過由通用目標檢測改為人臉檢測,即多分類改為2分類,且為小目標檢測。而且人臉檢測還加上了關鍵點檢測,可以依靠關鍵點增加召回率。主要思想還是依靠的通用目標檢測,除了yolo和R-CNN系列,還多出的一個算法系列為級聯網絡結構,MTCNN為其中一個代表。以前講過通用目標檢測的兩種方法,這次主要講一下級聯網絡的MTCNN這一種方案。
附帶一句我理解的閱讀論文思路。首先看綜述,梳理脈絡。之后找到經典方法即有重大突破的幾個節點,之后搜該方法的博客,明白大致意思。之后看論文,因為論文中細節更加豐富,之后結合論文,看源碼,進行復現。源碼是用的其他人的,非本人寫的
主要參考博客https://blog.csdn.net/qq_41044525/article/details/80820255
https://zhuanlan.zhihu.com/p/31761796
MTCNN分為三個網絡,P-net,R-Net,O-Net. 先來下預測部分總體流程圖(我只復現了預測部分源碼,不過會講一下loss函數)。

1. Stage 1

與訓練時不同,單預測部分來說,輸入圖片非固定大小,而是將原圖進行縮放,生成圖像金字塔,即系列的圖片,最小規格為12*12的。R-net網絡結構如圖,整體可看成將原圖整圖進行卷積(利用12*12,strides=2),之后生成預測的分類及位置偏移(無臉部關鍵點),網絡輸出中每一個1*1的方格映射到原圖的感受野為12*12。由此生成系列的預測值,由此可認為輸入為12*12大小。網絡部分代碼就不列出了,就一個網絡結構,很好理解。預處理代碼如下:
###作用是將原圖整圖輸入,生成圖像金字塔,輸入網絡中,以增加准確性。
factor_count=0
total_boxes=np.empty((0,9))
points=[]
h=img.shape[0]
w=img.shape[1]
minl=np.amin([h, w])
m=12.0/minsize
minl=minl*m
# creat scale pyramid
scales=[]
while minl>=12:
scales += [m*np.power(factor, factor_count)]
minl = minl*factor
factor_count += 1
# first stage ###將產生的最小為12*12系列的圖片送入pnet網絡中,獲得輸出的回歸框。scale為縮放比例,可以用來推測在原圖中的坐標
for j in range(len(scales)):
scale=scales[j]
hs=int(np.ceil(h*scale))
ws=int(np.ceil(w*scale))
im_data = imresample(img, (hs, ws)) ###此時輸入非12*12,而是每一個單元的感受野為12,這樣每一塊就生成了系列的預測框
im_data = (im_data-127.5)*0.0078125
img_x = np.expand_dims(im_data, 0)
img_y = np.transpose(img_x, (0,2,1,3))
out = pnet(img_y) ###輸入pnet網絡中,獲得輸出
out0 = np.transpose(out[0], (0,2,1,3)) ###二分類,即為人臉的概率
out1 = np.transpose(out[1], (0,2,1,3)) ###預測框偏移回歸 out0 size(1,H/12,W/12,2)
out1 size(1,H/12,W/12,4)
##輸出為第一層的預測框坐標合集,如何產生系列的預測框。
boxes, _ = generateBoundingBox(out1[0,:,:,1].copy(), out0[0,:,:,:].copy(), scale, threshold[0])
下面是genereteBoundingBox代碼.函數最用為輸出第一層每12*12大小的坐標,偏移,及預測得分
def generateBoundingBox(imap, reg, scale, t): ###reg為偏移 imag為是否為正類 scal縮小的比例 t 為閾值
# use heatmap to generate bounding boxes
stride=2
cellsize=12
#### 轉置計算很常見,目的只要為了方便比大小,做運算。通用目標檢測中也常用,對位置坐標進行轉置
imap = np.transpose(imap)
dx1 = np.transpose(reg[:,:,0])
dy1 = np.transpose(reg[:,:,1])
dx2 = np.transpose(reg[:,:,2])
dy2 = np.transpose(reg[:,:,3])
y, x = np.where(imap >= t) ###篩選出大於閾值的坐標。因為每個小單元格有一個預測概率值,四個坐標偏移值 H/12,W/12,y,x可看成index
if y.shape[0]==1:
dx1 = np.flipud(dx1)
dy1 = np.flipud(dy1)
dx2 = np.flipud(dx2)
dy2 = np.flipud(dy2)
score = imap[(y,x)] ###得分即為預測為人臉的概率,篩選大於閾值的預測框得分
reg = np.transpose(np.vstack([ dx1[(y,x)], dy1[(y,x)], dx2[(y,x)], dy2[(y,x)] ])) ###預測為滿足條件的人臉image預測框坐標偏移
if reg.size==0:
reg = np.empty((0,3))
bb = np.transpose(np.vstack([y,x]))
###為何*2+1?應該為*2+4? q1,q2值應為在原圖中每一個預測框的左上角,右下角坐標
q1 = np.fix((stride*bb+1)/scale)
q2 = np.fix((stride*bb+cellsize-1+1)/scale)
boundingbox = np.hstack([q1, q2, np.expand_dims(score,1), reg])
return boundingbox, reg ##返回每一個12*12塊大小的坐標及對應偏移及該塊得分
之后對預測值進行修正,預修剪,產生proposal的坐標,將預測出的回歸框部分提取出來,以便輸入到第二個網絡中。代碼如下
1 # inter-scale nms 對預測出的預測框進行nms,篩選預測框 2 pick = nms(boxes.copy(), 0.5, 'Union') 3 if boxes.size>0 and pick.size>0: 4 boxes = boxes[pick,:] 5 total_boxes = np.append(total_boxes, boxes, axis=0) 6 7 numbox = total_boxes.shape[0] ####篩選出的預測框個數 8 if numbox>0: 9 pick = nms(total_boxes.copy(), 0.7, 'Union') ###提高閾值,進一步進行nms 10 total_boxes = total_boxes[pick,:] 11 regw = total_boxes[:,2]-total_boxes[:,0] 12 regh = total_boxes[:,3]-total_boxes[:,1] 13 qq1 = total_boxes[:,0]+total_boxes[:,5]*regw 14 qq2 = total_boxes[:,1]+total_boxes[:,6]*regh 15 qq3 = total_boxes[:,2]+total_boxes[:,7]*regw 16 qq4 = total_boxes[:,3]+total_boxes[:,8]*regh 17 total_boxes = np.transpose(np.vstack([qq1, qq2, qq3, qq4, total_boxes[:,4]]))###依次為修正后的左上角,右下角坐標及該部分得分 18 total_boxes = rerec(total_boxes.copy()) ####使預測框變為正方形 19 total_boxes[:,0:4] = np.fix(total_boxes[:,0:4]).astype(np.int32) ##取整 20 dy, edy, dx, edx, y, ey, x, ex, tmpw, tmph = pad(total_boxes.copy(), w, h) #####對坐標進行修剪,使其不超出圖片大小
2. Stage 2

網絡結構如圖所示。此時的輸入必須為24*24大小,輸入的圖像為stage1產生的porposals,摳出來,輸入到stage2中。此處很像Faster-RCNN的第一步,初步篩選出純圖像,將不相關部分過濾掉。這也是級聯的意義。注,預測網絡輸出同樣只有得分與預測框坐標修正,無關鍵點信息。以下為代碼,與第一步很相似。
1 numbox = total_boxes.shape[0] 2 if numbox>0: ###由第一步得出的預測框,在原圖進行裁剪,resize,輸入到第R-Net中 3 # second stage 4 tempimg = np.zeros((24,24,3,numbox)) 5 for k in range(0,numbox): 6 tmp = np.zeros((int(tmph[k]),int(tmpw[k]),3)) 7 tmp[dy[k]-1:edy[k],dx[k]-1:edx[k],:] = img[y[k]-1:ey[k],x[k]-1:ex[k],:] 8 if tmp.shape[0]>0 and tmp.shape[1]>0 or tmp.shape[0]==0 and tmp.shape[1]==0: 9 tempimg[:,:,:,k] = imresample(tmp, (24, 24)) ####將porposalsresize成24*24大小。 10 else: 11 return np.empty() 12 tempimg = (tempimg-127.5)*0.0078125 13 tempimg1 = np.transpose(tempimg, (3,1,0,2)) 14 out = rnet(tempimg1) ###輸入到R-Net中得到輸出 15 out0 = np.transpose(out[0]) ####預測框坐標偏置 16 out1 = np.transpose(out[1]) ######預測得分 17 score = out1[1,:] 18 ##第一步中篩選出的預測框坐標。此時的坐標為原圖中的坐標偏移,並非resize之后的坐標偏置。即直接將偏移加到原圖中坐標即可 19 ipass = np.where(score>threshold[1]) 20 total_boxes = np.hstack([total_boxes[ipass[0],0:4].copy(), np.expand_dims(score[ipass].copy(),1)]) 21 mv = out0[:,ipass[0]] ###第二步得出的偏移值 22 if total_boxes.shape[0]>0: 23 pick = nms(total_boxes, 0.7, 'Union') 24 total_boxes = total_boxes[pick,:] ###先nms,第一步中調高閾值 25 total_boxes = bbreg(total_boxes.copy(), np.transpose(mv[:,pick])) ####加偏移后的坐標 26 total_boxes = rerec(total_boxes.copy())###變為正方形
3. Stage 3

第三步網絡沒什么好說的,和第二步一樣的流程。不過此時多了一個關鍵點預測。最后得到了結果。
1 numbox = total_boxes.shape[0] 2 if numbox>0: 3 # third stage ###仿照第二步,將第二步得出的預測圖像輸入到第三個網絡中 4 total_boxes = np.fix(total_boxes).astype(np.int32) 5 dy, edy, dx, edx, y, ey, x, ex, tmpw, tmph = pad(total_boxes.copy(), w, h) 6 tempimg = np.zeros((48,48,3,numbox)) 7 for k in range(0,numbox): 8 tmp = np.zeros((int(tmph[k]),int(tmpw[k]),3)) 9 tmp[dy[k]-1:edy[k],dx[k]-1:edx[k],:] = img[y[k]-1:ey[k],x[k]-1:ex[k],:] 10 if tmp.shape[0]>0 and tmp.shape[1]>0 or tmp.shape[0]==0 and tmp.shape[1]==0: 11 tempimg[:,:,:,k] = imresample(tmp, (48, 48)) 12 else: 13 return np.empty() 14 tempimg = (tempimg-127.5)*0.0078125 15 tempimg1 = np.transpose(tempimg, (3,1,0,2)) 16 out = onet(tempimg1) 17 out0 = np.transpose(out[0]) 18 out1 = np.transpose(out[1]) 19 out2 = np.transpose(out[2]) 20 score = out2[1,:] 21 points = out1 22 ipass = np.where(score>threshold[2]) 23 points = points[:,ipass[0]] 24 total_boxes = np.hstack([total_boxes[ipass[0],0:4].copy(), np.expand_dims(score[ipass].copy(),1)]) 25 mv = out0[:,ipass[0]] 26 27 w = total_boxes[:,2]-total_boxes[:,0]+1 28 h = total_boxes[:,3]-total_boxes[:,1]+1 29 points[0:5,:] = np.tile(w,(5, 1))*points[0:5,:] + np.tile(total_boxes[:,0],(5, 1))-1 30 points[5:10,:] = np.tile(h,(5, 1))*points[5:10,:] + np.tile(total_boxes[:,1],(5, 1))-1 31 if total_boxes.shape[0]>0: 32 total_boxes = bbreg(total_boxes.copy(), np.transpose(mv)) 33 pick = nms(total_boxes.copy(), 0.7, 'Min') 34 total_boxes = total_boxes[pick,:] 35 points = points[:,pick] 36 37 return total_boxes, points ####得出最終的預測值
此處只有預測部分的代碼,在訓練時,有一個與其他目標檢測不同的地方在於,有5個人關鍵點檢測,也計入了損失函數中,以此進行訓練,可增加識別的准確性。
來勉強解讀一下。。這部分看的很粗略。
1. 臉分類損失函數。交叉熵損失函數

2. 預測框損失函數。 平方損失

3. 關鍵點損失函數。同樣為平方損失

3. 綜合訓練,整體損失函數。每部分網絡的權重不同。

最后再添加一部分內容,即人臉對齊與識別.看到一段別人代碼,根據眼睛記性對齊。我不太理解為什么這么做,因為opencv就有原裝函數可以做對齊,不知道為何還要用PIL麗麗的函數。具體過程就是通過使兩個眼睛在一條水平線上,從而對圖像進行仿射變換,還有進一步對圖像進行裁剪。復代碼
# -*- coding: utf-8 -*-
"""
Created on Thu Jan 1 16:09:32 2015
@author: crw
"""
# 參數含義:
# CropFace(image, eye_left, eye_right, offset_pct, dest_sz)
# eye_left is the position of the left eye
# eye_right is the position of the right eye
# 比例的含義為:要保留的圖像靠近眼鏡的百分比,
# offset_pct is the percent of the image you want to keep next to the eyes (horizontal, vertical direction)
# 最后保留的圖像的大小。
# dest_sz is the size of the output image
#
import sys,math,Image
# 計算兩個坐標的距離
def Distance(p1,p2):
dx = p2[0]- p1[0]
dy = p2[1]- p1[1]
return math.sqrt(dx*dx+dy*dy)
# 根據參數,求仿射變換矩陣和變換后的圖像。
def ScaleRotateTranslate(image, angle, center =None, new_center =None, scale =None, resample=Image.BICUBIC):
if (scale is None)and (center is None):
return image.rotate(angle=angle, resample=resample)
nx,ny = x,y = center
sx=sy=1.0
if new_center:
(nx,ny) = new_center
if scale:
(sx,sy) = (scale, scale)
cosine = math.cos(angle)
sine = math.sin(angle)
a = cosine/sx
b = sine/sx
c = x-nx*a-ny*b
d =-sine/sy
e = cosine/sy
f = y-nx*d-ny*e
return image.transform(image.size, Image.AFFINE, (a,b,c,d,e,f), resample=resample)
# 根據所給的人臉圖像,眼睛坐標位置,偏移比例,輸出的大小,來進行裁剪。
def CropFace(image, eye_left=(0,0), eye_right=(0,0), offset_pct=(0.2,0.2), dest_sz = (70,70)):
# calculate offsets in original image 計算在原始圖像上的偏移。
offset_h = math.floor(float(offset_pct[0])*dest_sz[0])
offset_v = math.floor(float(offset_pct[1])*dest_sz[1])
# get the direction 計算眼睛的方向。
eye_direction = (eye_right[0]- eye_left[0], eye_right[1]- eye_left[1])
# calc rotation angle in radians 計算旋轉的方向弧度。
rotation =-math.atan2(float(eye_direction[1]),float(eye_direction[0]))
# distance between them # 計算兩眼之間的距離。
dist = Distance(eye_left, eye_right)
# calculate the reference eye-width 計算最后輸出的圖像兩只眼睛之間的距離。
reference = dest_sz[0]-2.0*offset_h
# scale factor # 計算尺度因子。
scale =float(dist)/float(reference)
# rotate original around the left eye # 原圖像繞着左眼的坐標旋轉。
image = ScaleRotateTranslate(image, center=eye_left, angle=rotation)
# crop the rotated image # 剪切
crop_xy = (eye_left[0]- scale*offset_h, eye_left[1]- scale*offset_v) # 起點
crop_size = (dest_sz[0]*scale, dest_sz[1]*scale) # 大小
image = image.crop((int(crop_xy[0]),int(crop_xy[1]),int(crop_xy[0]+crop_size[0]),int(crop_xy[1]+crop_size[1])))
# resize it 重置大小
image = image.resize(dest_sz, Image.ANTIALIAS)
return image
if __name__ =="__main__":
image = Image.open("/media/crw/DataCenter/Dataset/CAS-PEAL-R1/POSE/000001/MY_000001_IEU+00_PD-22_EN_A0_D0_T0_BB_M0_R1_S0.tif")
leftx =117
lefty=287
rightx=187
righty= 288
CropFace(image, eye_left=(leftx,lefty), eye_right=(rightx,righty), offset_pct=(0.1,0.1), dest_sz=(200,200)).save("test_10_10_200_200.jpg")
CropFace(image, eye_left=(leftx,lefty), eye_right=(rightx,righty), offset_pct=(0.2,0.2), dest_sz=(200,200)).save("test_20_20_200_200.jpg")
CropFace(image, eye_left=(leftx,lefty), eye_right=(rightx,righty), offset_pct=(0.3,0.3), dest_sz=(200,200)).save("test_30_30_200_200.jpg")
CropFace(image, eye_left=(leftx,lefty), eye_right=(rightx,righty), offset_pct=(0.4,0.4), dest_sz=(200,200)).save("test_40_40_200_200.jpg")
CropFace(image, eye_left=(leftx,lefty), eye_right=(rightx,righty), offset_pct=(0.45,0.45), dest_sz=(200,200)).save("test_45_45_200_200.jpg")
CropFace(image, eye_left=(leftx,lefty), eye_right=(rightx,righty), offset_pct=(0.2,0.2)).save("test_20_20_70_70.jpg")
---------------------
作者:RiweiChen
來源:CSDN
原文:https://blog.csdn.net/chenriwei2/article/details/42320021
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!
圖中較難理解的函數為image.transform用來做仿射變換的,如果不理解的話可參考這個https://stackoverflow.com/questions/17056209/python-pil-affine-transformation
附上opencv自帶用來做仿射變換的函數 ,雖然粗糙,但應該夠用了,不用上面那么麻煩的代碼。封裝好的函數好處就是可直接算出坐標變換矩陣。如何使用參考
https://blog.csdn.net/a352611/article/details/51418178
RotateMatrix = cv2.getRotationMatrix2D(center=(Img.shape[1]/2, Img.shape[0]/2), angle=90, scale=1)
RotImg = cv2.warpAffine(Img, RotateMatrix, (Img.shape[0]*2, Img.shape[1]*2))
最后,說一下人臉識別。 人臉識別即判斷兩個圖片是不是同一個人。具體過程分三部分:
1. 采樣,將圖片輸入到MTCNN中,得到人臉框,進行對齊,之后裁剪。
2. 將第一步裁剪出的圖像輸入到網絡中,一般為Inception網絡, 得到輸出向量。
3.有兩種處理結果方式,一種是三元損失函數,即 若 x1,x2為同一個人的,y為另一個人的,則 (x1-x2)-(x1-y)必然是小於0的,損失為0,可看成計算兩個向量的相似度。這種方法大大減少了計算過程,需要的數據量也少。另為一種為聚類,也是通過計算兩個向量之間的具體來計算損失的。一般采用第一種方法。
說到這里,我提下自己的理解。對於人臉對齊,和人臉識別,其實主要用到了矩陣變換的知識,這塊要把矩陣的意義弄得比較明白。可參考西瓜書的聚類章節,對各種距離解釋的很清楚,說到底就是矩陣變化,純數學計算,沒什么新內容。時間充裕可以看一下,不然會用那幾個函數就行了,opencv及python庫太多了
