人脸识别——对齐算法

Posted on 2017-09-05(星期二) 22:24 in Data

基本思路:

使用facenet框架,可以识别出一张图片中的人脸头像,并返回每张人脸的边界和五官坐标,但图片中有的头像是有倾斜角度的,我们将倾斜度数小于指定阈值的进行校正,其余的按原形截取图片。

  • 倾斜度数根据两只眼睛的坐标加以计算;
  • 缩放比为两眼X坐标与两眼之间的距离比值;
  • 将人脸边界大小分别向四个方向扩充对应边的1/4长,也就是两边界长宽扩为1.5倍/缩放比;
  • 截取扩充的区域,然后以鼻子坐标为中心进行旋转(也可以考虑以左眼为中心旋转,但以有效数据的中点进行旋转可降低扩充的面积);
  • 旋转后鼻子的X、Y坐标为原坐标减左边界和上边界;
  • 旋转后需要根据两眼和鼻子构成的三角形算出旋转后左眼的坐标,用于最后截图中心点的定位(根据余弦定理和勾股定理计算);
  • 根据两眼的斜率和人脸边界大概可以计算两眼连线之上有效高度,用于确定最后图片上边界(默认认为脸长大于脸宽);
  • 脸的长度根据人脸边界/斜率得出大概值(偏小);
  • 总的图片高度根据计算出的脸长和margin值相加得来;
  • 根据左眼坐标和图片高度计算最后截图需要的四个边界值,得到最后需要图,最后进行缩放完成。

算法实现:

import facenet
import math
from PIL import Image

def alignFace(self, image, boundingBoxes, positions, cropSize=160, angleThreshold=5):
    '''
image 原始图像
boundingBoxes 脸部轮廓边框
positions 五官坐标
cropSize:最后保留的图像的大小 
angleThreshold: 旋转头像倾斜角度阈值
    '''
    aligneds=[]
    if len(positions.shape) < 2:
        return aligneds
    for i in range(positions.shape[1]):
        eyeLeft, eyeRight=(positions[0,i], positions[5,i]), (positions[1,i], positions[6,i])
        nosePoint = (positions[2,i], positions[7,i])
        eye_direction = (eyeRight[0]- eyeLeft[0], eyeRight[1]- eyeLeft[1])  
        eyeDist = self.__distance(eyeLeft, eyeRight)
        leftEyeNoseDist = self.__distance(eyeLeft, nosePoint)
        rightEyeNoseDist = self.__distance(nosePoint, eyeRight)
        slope = math.fabs(eye_direction[1]/eye_direction[0])
        rate = math.fabs(eye_direction[0]/eyeDist)

        #图片倾斜度超过阈值时旋转
        if slope >= math.tan(angleThreshold*math.pi/180):
            #根据boundingBox和斜度截取小图
            boxWidth,boxHeight = (boundingBoxes[i,2]-boundingBoxes[i,0]),(boundingBoxes[i,3]-boundingBoxes[i,1])
            left,upper = (boundingBoxes[i,0]-boxWidth/4/rate),(boundingBoxes[i,1]-boxHeight/4/rate)
            right,lower = (boundingBoxes[i,2]+boxWidth/4/rate),(boundingBoxes[i,3]+boxHeight/4/rate)
            img = Image.fromarray(image).crop((left, upper, right, lower))
            img.save('D:/test_data/face/crop' +str(i)+ '.jpg')

            #以鼻子为中心旋转截图
            nosePoint = ((nosePoint[0]-left), (nosePoint[1]-upper))
            rotation = -math.atan2(float(eye_direction[1]),float(eye_direction[0]))
            img = self.__scaleRotateTranslate(img, center=nosePoint, angle=rotation) 
            img.save('D:/test_data/face/rotate' +str(i)+ '.jpg')

            #计算旋转之后左眼坐标
            cosLeftEye = math.fabs((math.pow(eyeDist,2)+math.pow(leftEyeNoseDist,2)-math.pow(rightEyeNoseDist,2))/(2*eyeDist*leftEyeNoseDist))
            rotatedLfEye = ((nosePoint[0]-leftEyeNoseDist*cosLeftEye), (nosePoint[1]-math.sqrt(math.pow(leftEyeNoseDist,2)-math.pow(leftEyeNoseDist*cosLeftEye,2))))

            #截取正面脸部图像
            faceLen = (int(math.fabs(boundingBoxes[i,1]-boundingBoxes[i,3])))/rate
            margin = ((boxWidth+boxHeight)//60)*math.pow(rate,3)
            finalLen = faceLen+2*margin
            upEyeHeight = self.__getUpEyeHeight(eyeLeft, eyeRight, eyeDist, boundingBoxes[i,1])
            left,upper = (rotatedLfEye[0]+eyeDist/2-finalLen/2), (rotatedLfEye[1]-upEyeHeight-margin)
            right,lower = left+finalLen, upper+finalLen
            img = img.crop((left, upper, right, lower))  
            img = img.resize((cropSize, cropSize), Image.ANTIALIAS)
            img.save('D:/test_data/face/duoren' +str(i)+ '.jpg')
            aligneds.append(np.array(img)) 
        else:#原图截图
            img_size = np.asarray(image.shape)[0:2]
            margin=self.__computerMargin(boundingBoxes[i])
            det = np.squeeze(boundingBoxes[i])
            bb = np.zeros(4, dtype=np.int32)
            bb[0] = np.maximum(det[0]-margin/2, 0)
            bb[1] = np.maximum(det[1]-margin/2, 0)
            bb[2] = np.minimum(det[2]+margin/2, img_size[1])
            bb[3] = np.minimum(det[3]+margin/2, img_size[0])
            cropped = image[bb[1]:bb[3],bb[0]:bb[2],:]
            aligned = misc.imresize(cropped, (cropSize, cropSize), interp='bilinear')
            aligneds.append(aligned)
    return aligneds

最后效果如下:

原图:

对齐:


原图:

对齐:


原图:

对齐:

总结:

该算法取图片长度的算法取决于facenet返回的人脸边界和脸的斜率,在斜率不是太高(40度以下)的时候表现还好。

改进:

  • 随着斜率增大,图片下方,也就是下巴下部空闲部分会多一些;
  • 脸旁两部分无用数据较多,如果是贴脸照片会多出干扰信息,这个也可根据人脸边界进行改进修正。