人脸特征点检测(一)

人脸特征点检测(Facial landmark detection),即人脸特征点定位、人脸对齐(Face Alignment),是在人脸检测的基础上进行的,对人脸上的特征点例如嘴角、眼角等进行定位。
人脸特征点检测有很多用途,例如:
(1)改善人脸识别:通过人脸特征点检测将人脸对齐到平均脸,对齐后的人脸图像识别算法更加有效。
(2)人脸平均:利用人脸特征点检测的结果,将多个人脸进行融合成新的平均人脸。尝试做了一下2017年巴塞罗那足球俱乐部FCB一线队所有成员的平均脸,如下图,哈哈,很帅有木有?!

这里写图片描述

(3)人脸交换:利用人脸特征点检测的结果,对两张人脸进行无缝换脸,将一张人脸换到另一张上,做了下把贝克汉姆的脸换到梅西上的效果,如下图所示。

这里写图片描述这里写图片描述这里写图片描述

(4)人脸化妆&装扮:这方面的应用很多,感觉也是最具有商业应用价值的。可以做很多有趣的事情,日常生活中常见的,例如给你的脸上加上猫猫狗狗的小胡须、兔耳朵,涂上腮红、带上圣诞帽等装扮。还有美图秀秀美妆相机、美颜相机等,例如粉底、腮红、唇彩、眼影眼线、睫毛、双眼皮、美瞳、亮眼、大眼、鼻子高挺、自动瘦脸、眉毛等人脸化妆,都是在人脸特征点检测的基础上实现的。不得不说,现在的PS技术很强大,而且还是提供了傻瓜式的,用户量很大…

上述这些人脸特征点检测的应用,说明特征点的检测确实很有用很重要。特征点检测的又快又准,接下来的工作才好开展。
论文Facial Landmark Detection by Deep Multi-task Learning对人脸特征点检测有很好的效果,如下图所示,鲁棒性很强,但只公布了演示程序,没有公布源码及提供使用借口,无法实际使用,且论文实现和训练起来难度很大。

这里写图片描述

在Happynear大神github主页有论文Joint Face Detection and Alignment using Multi-task Cascaded Convolutional Neural Networks的实现代码,暂时还没用到。

Seetaface中科院计算所山世光研究员带领的人脸识别研究组研发,代码基于C++实现,不依赖第三方库,开源免费,其人脸对齐模块支持5个人脸关键点的检测,其采用的是一种由粗到精的自编码器网络(Coarse-to-Fine Auto-encoder Networks, CFAN)来求解这个复杂的非线性映射过程。Dlib库实现了2014年一篇非常经典的人脸特征点检测的论文:Face Alignment at 3000 FPS via Regression Local Binary Features,其人脸特征点检测又快又准。深圳大学于仕祺老师公布的免费的libfacedetect,人脸特征点检测也非常快,效果也不错,和Dlib一样为68特征点检测,但鲁棒性不如Dlib。Seetaface、Dlib和libfacedetect都提供了人脸特征点检测的接口。

下面仅介绍三种方式来实现人脸特征点检测。

1.级联回归CNN人脸特征点检测
2.Dlib人脸特征点检测
3.libfacedetect人脸特征点检测
4.Seetaface人脸特征点检测方法


1.级联回归CNN人脸特征点检测

采用该Cascade级联回归CNN方法来定位一个人脸中的5个特征点,在我的机器上(GTX 1060)耗时7ms,算比较快了(然而,dlib、libfacedetect等做人脸68个特征点检测的速度比这都还要快…),目前人脸特征点检测的耗时主要还是在之前的要做的人脸检测上。用caffe训练网络实现该方法所用到的数据集样本、制作数据集和预测特征点的python脚本打包地址:下载链接

人脸特征点检测实际上是在人脸检测的基础上,在人脸框中预测特征点的位置。很多人脸数据集都提供了图像样本中人脸框的位置及特征点的坐标,我们需要做的是训练能预测特征点在人脸框中相对位置的网络。在实际预测时,我们首先通过人脸检测方法获取人脸框位置,然后在人脸框中预测特征点坐标。
卷积神经网络可以用于分类和回归任务,做分类任务时最后一个全连接层的输出维度为类别数,接着Softmax层采用Softmax Loss计算损失函数,而如果做回归任务,最后一个全连接层的输出维度则是要回归的坐标值的个数,采用的是欧几里何损失Euclidean Loss。

这里写图片描述

训练卷积神经网络来回归特征点坐标,这里博主只做了人脸中5个特征点的检测(如上图所示)。如果只采用一个网络来做回归训练,会发现得到的特征点坐标并不够准确,为了更加快速、准确的定位人脸特征点,采用级联回归CNN的方法,借鉴级联CNN中的级联思想,进行分段式特征点定位,其具体思路为:
(1)首先在整个人脸图像(蓝色框)上训练一个网络来对人脸特征点坐标进行粗回归,实际采用的网络其输入大小为39x39的人脸区域灰度图,预测时可以得到特征点的大致位置。
(2)设计另一个回归网络,以人脸特征点周围的局部区域图像(红色框)作为输入进行训练,实际采用的网络其输入大小为15x15的特征点局部区域灰度图,以预测到更加准确的特征点位置。
需要注意的是,由于采用的是欧几里何损失,在计算坐标时,使用的是相对坐标而不是绝对坐标,例如,在(1)中使用的是鼻子点在人脸框(蓝色框)中的相对坐标(0~1),在(2)中使用的是鼻子点在选定的周围区域框(红色框)中的相对坐标,这样能够促进模型收敛,避免网络训练发散。

这里写图片描述

在理解思路后,准备制作数据集并设计或选取网络了,首先是数据集制作。采用的是MTFL人脸数据库,在data\face_fp文件夹下,如图lfw_5590和net_7876文件夹中包括了所有的样本(包括训练集和验证集),训练集和测试集的标签文本trainImageList.txt或testImageList.txt中的每一行,依次对应图像路径、人脸框坐标值和五个特征点的坐标值标签,具体参照Readme.txt。

这里写图片描述
这里写图片描述

在第一阶段训练时,对数据集进行了增广(只针对训练集),除了做镜像之外,还对人脸框位置做了两组缩放和四组平移(实际检测时检测出到的人脸框位置可能不够准确,为了克服这种影响,提高泛化能力),然后将图像中的人脸框区域裁剪出来,并统一缩放到39x39大小,这样数据增广了3x5=15倍,会增加训练耗时,但不影响测试时间。事实证明以上的数据增广使得第一阶段预测的特征点更加准确,实际上博主还尝试了对人脸框做两组随机的小角度旋转,但最后对特征点位置预测的准确性并没有多大提高。在做数据增广的时候,对应的特征点坐标也要变换过来,而且要转化为相对坐标(第一阶段是相对人脸框,0~1)。
使用caffe训练CNN网络,由于是回归问题,多标签,而lmdb不支持多标签(虽然可以修改caffe源码以支持多标签,但这里没有必要),因此使用hdf5格式以支持多标签,在data\face_fp下的stage1.py脚本可以执行生成第一阶段的经过数据增广的hdf5格式的训练集和验证集以及对应的标签文本,输出到data\face_fp\1F文件夹下。

# -*- coding: utf-8 -*-
"""
Created on Mon May 15 21:34:35 2017

@author: Administrator
"""
import os
from os.path import join, exists
import cv2
import numpy as np
import h5py
from common_utils import shuffle_in_unison_scary, logger,processImage, getDataFromTxt, BBox
from utils import flip, rotate
import time

###第一阶段,大致确定关键点位置
TRAIN = './'
OUTPUT = './1_F'

if not exists(OUTPUT): 
    os.mkdir(OUTPUT)
assert(exists(TRAIN) and exists(OUTPUT))

###生成hdf5文件,训练集做数据增广
def generate_hdf5(ftxt, output, mode='train', augment=False): #输入参数:(原始图像和关键点坐标标签文本,h5文件输出目录,h5文件名,是否数据增广)

    data = getDataFromTxt(ftxt) #读取存放了文件路径和人脸框及其关键点的标签文本,坐标转换成相对坐标,返回读取结果(图像完整路径,人脸框,关键点绝对坐标)
    F_imgs = [] #人脸框图
    F_landmarks = [] #相对坐标  

    if not augment: #如果不做数据增广
        for (imgPath, bbox, landmarkGt) in data:
            img = cv2.imread(imgPath)
            assert(img is not None) #检查img是否存在
            logger("process %s" % imgPath) #打印信息
            gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)           
            f_bbox = bbox
            f_face = gray[f_bbox.top:f_bbox.bottom+1,f_bbox.left:f_bbox.right+1] #人脸框图像               
            landmarkGt_p = f_bbox.projectLandmark(landmarkGt) #转换成相对人脸框相对坐标

            ### 原图
            f_face = cv2.resize(f_face, (39, 39))       
            F_imgs.append(f_face.reshape((1, 39, 39)))
            F_landmarks.append(landmarkGt_p.reshape(10))  

    else:
        for (imgPath, bbox, landmarkGt) in data:
            img = cv2.imread(imgPath)
            assert(img is not None) #检查img是否存在
            logger("process %s" % imgPath) #打印信息
            gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)       
            height,width = gray.shape

            for exp in range(3): #5x3共15种变换,3种外扩
                bbox_e = bbox.expand(0.1*exp)  #分别往外扩0.0,0.1,0.2                               
                for ori in range(5): #5种平移                    
                    if ori == 1:
                        bbox_s = bbox_e.subBBox(0.1,1.1,0.0,1.0) #向右平移0.1
                    elif ori == 2:
                        bbox_s = bbox_e.subBBox(-0.1,0.9,0.0,1.0) #向左平移0.1
                    elif ori == 3:
                        bbox_s = bbox_e.subBBox(0.0,1.0,0.1,1.1) #向下平移0.1
                    elif ori == 4:
                        bbox_s = bbox_e.subBBox(0.0,1.0,-0.1,0.9) #向上平移0.1
                    else:
                        bbox_s = bbox_e                

                    f_bbox = BBox([int(bbox_s.left),int(bbox_s.right),int(bbox_s.top),int(bbox_s.bottom)]) #人脸框
                    if (f_bbox.top < 0 or f_bbox.left < 0 or f_bbox.bottom + 1 > height or f_bbox.right + 1 > width) : #如果人脸框超出图像边界,忽略之
                        continue
                    f_face = gray[f_bbox.top:f_bbox.bottom+1,f_bbox.left:f_bbox.right+1] #人脸框图像               
                    landmarkGt_p = f_bbox.projectLandmark(landmarkGt) #转换成相对人脸框相对坐标

                    #水平镜像
                    face_flipped, landmark_flipped = flip(f_face, landmarkGt_p) #将人脸框图像和关键点坐标同时镜像

                    face_flipped = cv2.resize(face_flipped, (39, 39)) #人脸框图像缩放到统一大小,默认双线性插值
                    F_imgs.append(face_flipped.reshape((1, 39, 39))) #opencv读取的图像shape为(h,w,c),转变为(c,h,w)
                    F_landmarks.append(landmark_flipped.reshape(10)) #将5x2的标签reshape成一维
                    ### 原图
                    f_face = cv2.resize(f_face, (39, 39))       
                    F_imgs.append(f_face.reshape((1, 39, 39)))
                    F_landmarks.append(landmarkGt_p.reshape(10))
    length = len(F_imgs)
    print 'length = %d' % length
    F_imgs, F_landmarks = np.asarray(F_imgs), np.asarray(F_landmarks) #转化成array
    F_imgs = processImage(F_imgs) #图像预处理:去均值、归一化
    shuffle_in_unison_scary(F_imgs, F_landmarks) #乱序

    logger("generate %s" % output) #打印日志    
    num = length / 100000
    h5files = []
    for index in range(num):
        suffix = '_%d.h5' % index
        h5file = join(output,mode + suffix) #拼接成h5文件全路径
        h5files.append(h5file)
        with h5py.File(h5file, 'w') as h5: #以“写”方式打开h5文件
            h5['data'] = F_imgs[index*100000 : (index + 1)*100000].astype(np.float32) #数据转换成float32类型,存图像
            h5['landmark'] = F_landmarks[index*100000 : (index + 1)*100000].astype(np.float32) #数据转换成float32类型,存坐标标签

    suffix = '_%d.h5' % num
    h5file = join(output,mode + suffix) #拼接成h5文件全路径
    h5files.append(h5file)
    with h5py.File(h5file, 'w') as h5: 
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值