分享

【Caffe实践】基于Caffe的人脸关键点检测实现

 openlog 2015-12-06

引言

如果关注Kaggle 机器学习项目的同学,一定很熟悉人脸关键点检测这个任务,在2013 年的时候,ICML举办一个的challgene,现在放在kaggle 上作为 一种最常规kaggle入门任务而存在。

本文的主要目的在于验证深度学习模型在人脸点检测效果,踩踩里面的坑。

任务介绍

人脸关键点检测,也称之为人脸点检测,是在一张已经被人脸检测器检测到的人脸图像中,再进一步检测出五官等关键点的二维坐标信息,以便于后期的人脸对齐(face alignment)任务。

根据不同的任务,需要检测的关键点数目有多有少,有些仅要求检测2只眼睛的坐标位置,有些要求检测眼睛、嘴巴、鼻子的5个坐标位置,还有更多的,68个位置,它包含了五官的轮廓信息。
如图所示:

根据任务,可以把要学习的模型函数表示为:

Y=FX,W

其中,X 是输入的人脸图像,W是我们要学习的模型参数,Y[(x1,y1),(x2,y2),(x3,y3),(x4,y4),(x5,y5)] 是我们需要检测的人脸点坐标位置。

这是一个典型的回归问题,可以采用最简单的平方误差损失函数,然后用机器学习方法学习这个模型。

Loss=15i=15((xi?xi)2+(yi?yi)2)

其中(xi,yi)为预测的位置,(xi,yi) 为标注的关键点位置。

很显然,也很容易的就将该任务放到caffe中进行学习。

实验过程:

数据准备

由于港中文[1]他们有公开了训练集,所以我们就可以直接使用他们提供的图像库就好了。
数据是需要转化的:

1、框出人脸图像部分,从新计算关键点的坐标
2、缩放人脸框大小,同时更新计算的关键点坐标
3、一些数据增强处理:我只采样了左右对称的增强方法(还可以采用的数据增强方法有旋转图像,图像平移部分)
转换脚本如下:

# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
"""
Created on Wed Nov 04 16:36:33 2015
@author: RiweiChen <riwei.chen@outlook.com>
"""
import skimage
import skimage.io
import numpy as np
import cv2

def face_draw_point(filelist,savePath):
    '''
    人脸的剪切
    '''
    fid = open(filelist)
    lines = fid.readlines()
    fid.close()
    for line in lines:
        words = line.split(' ')
        filename = words[0]
        #im=skimage.io.imread(filename)
        im=cv2.imread(filename)
        #保存人脸的点,需要经过转换
        point = np.zeros((10,))

        point[0]=float(words[5])
        point[1]=float(words[6])
        point[2]=float(words[7])
        point[3]=float(words[8])
        point[4]=float(words[9])
        point[5]=float(words[10])
        point[6]=float(words[11])
        point[7]=float(words[12])
        point[8]=float(words[13])
        point[9]=float(words[14])

        for i in range(0,10,2):
            #skimage.draw.circle(point[i+1],point[i])
            cv2.circle(im, (int(point[i]),int(point[i+1])), 5, [0,0,255])            

        #skimage.io.imsave(savePath+filename,imcrop)
        cv2.imwrite(savePath+filename,im)
        #print words[0]
        #print words[1]


def face_prepare(filelist,fileout,savePath,w,h):
    '''
    人脸的剪切

    @debug: 谢谢网友@cyq0122 指出图像镜像过后,左右眼睛和嘴角都需要跟着改变的大BUG
    '''
    fid = open(filelist)
    lines = fid.readlines()
    fid.close()

    fid = open(fileout,'w')

    count = 1
    for line in lines:
        words = line.split(' ')
        filename = words[0]
        #im=skimage.io.imread(filename)
        im=cv2.imread(filename)
        print np.shape(im)
        x1 = int(words[1])
        y1 = int(words[2])
        x2 = int(words[3])
        y2 = int(words[4])
        #缩放的比例
        rate = float(y1-x1)/w
        #imcrop = im[x2:y2,x1:y1,:]
        imcrop = im[x2:y2,x1:y1,:]
        print np.shape(imcrop)
        #保存人脸的点,需要经过转换
        point = np.zeros((10,))

        point[0]=(float(words[5])-x1)/rate
        point[1]=(float(words[6])-x2)/rate
        point[2]=(float(words[7])-x1)/rate
        point[3]=(float(words[8])-x2)/rate
        point[4]=(float(words[9])-x1)/rate
        point[5]=(float(words[10])-x2)/rate
        point[6]=(float(words[11])-x1)/rate
        point[7]=(float(words[12])-x2)/rate
        point[8]=(float(words[13])-x1)/rate
        point[9]=(float(words[14])-x2)/rate
        imcrop = cv2.resize(imcrop,(w,h))
        #原始图像
        fid.write(str(count)+'.jpg')
        for i in range(0,10,2):
            #cv2.circle(imcrop, (int(point[i]),int(point[i+1])), 5, [0,0,255])  
            fid.write('\t'+str(point[i]))
            fid.write('\t'+str(point[i+1]))
        fid.write('\n')  

        # 翻转图像
        # @cyq0122 指出,左右眼睛需要交换,嘴巴也一样。
        imcrop_flip = cv2.flip(imcrop,1)
        fid.write(str(count)+'_flip.jpg')      
        fid.write('\t'+str(w-point[2]-1))
        fid.write('\t'+str(point[3]))
        fid.write('\t'+str(w-point[0]-1))
        fid.write('\t'+str(point[1]))
        fid.write('\t'+str(w-point[4]-1))
        fid.write('\t'+str(point[5]))
        fid.write('\t'+str(w-point[8]-1))
        fid.write('\t'+str(point[9]))
        fid.write('\t'+str(w-point[6]-1))
        fid.write('\t'+str(point[7]))
        fid.write('\n') 
        #skimage.io.imsave(savePath+filename,imcrop)
        #cv2.imwrite(savePath+filename,imcrop)
        cv2.imwrite(savePath+str(count)+'_flip.jpg',imcrop_flip)
        cv2.imwrite(savePath+str(count)+'.jpg',imcrop)
        count = count + 1
    fid.close()
        #print words[0]
        #print words[1]



if __name__ == "__main__":
    #train
    w = 39
    h = 39
    filelist=r"F:\Dataset\FacePoints\train\trainImageList.txt"
    filelistesave = r"F:\\MyDataset\\FacePoint\\train39X39\\train.list"
    savePath='F:\\MyDataset\\FacePoint\\train39X39\\'

    face_prepare(filelist,filelistesave,savePath,w,h)
    filelist=r"F:\Dataset\FacePoints\train\testImageList.txt"
    filelistesave = r"F:\\MyDataset\\FacePoint\\test39X39\\test.list"
    savePath='F:\\MyDataset\\FacePoint\\test39X39\\'
    face_prepare(filelist,filelistesave,savePath,w,h)
    #face_draw_point(filelist,savePath)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138

数据转化

跟以往的图像分类采用LMDB或者LevelDB 作为数据处理不同,这里我们的标签是一个向量,而不是一个值,所以不能直接用LMDB来作为存储,还好,caffe提供了另外一种数据存储方法来处理这种需求,那就是HDF5, 直观的差别就在于LMDB的标签是一个数,而HDF5 的标签可以是任意(blob),也就是说对于分类任务,我们也可以采样HDF5作为存储的数据模式,但是HDF5直接读取文件,相比LMDB 速度上有所损失。

脚本如下:

# -*- coding: utf-8 -*-
"""
Created on Wed Apr 29 20:53:14 2015

@author: crw
"""

import os
import random
import h5py
import numpy as np
from skimage import io
from skimage import transform as tf
trainpairlist=''
testpairlist=''
def savehdf5(X,Y,filename):
    with h5py.File(filename, 'w') as f:
        f['data'] = X
        f['label'] = Y
        print 'having saving a hdf5 file !'

def convert(source_path,pairlist,savepath,hdf5list,w,h):
    '''
    @brief: 将图像列表里的图像转化为矩阵,

    @return: X,Y
    '''
    step = 5000

    fid=open(pairlist)
    lines= fid.readlines()
    fid.close()
    X=np.empty((step,3,w,h),dtype=np.float)
    Y=np.empty((step,10,1,1),dtype=np.float)
    i=0
    t=1
    #记得HDF5需要实现shuffle.
    random.shuffle(lines)
    for line in lines:
        words=line.split('\t')
        inputimage=words[0]
        #image  标签
        points = np.zeros((10,))

        points[0]=float(words[1])
        points[1]=float(words[2])
        points[2]=float(words[3])
        points[3]=float(words[4])
        points[4]=float(words[5])
        points[5]=float(words[6])
        points[6]=float(words[7])
        points[7]=float(words[8])
        points[8]=float(words[9])
        points[9]=float(words[10])

        im=io.imread(source_path+inputimage,as_grey=False)
        im=tf.resize(im,(w,h))

        X[i,0,:,:]=im[:,:,0]
        X[i,1,:,:]=im[:,:,1]
        X[i,2,:,:]=im[:,:,2]
        Y[i,:,0,0]=points
        i=i+1
        if i==step:
            filename = os.path.join(savepath, str(t)+ '.h5')
            savehdf5(X,Y,filename)
            with open(os.path.join(savepath,hdf5list), 'a') as f:
                f.write(filename + '\n')
            i=0
            t=t+1
    if i > 0:
        filename = os.path.join(savepath, str(t)+ '.h5')
        savehdf5(X[0:i,:,:,:],Y[0:i,:,:,:],filename)
        with open(os.path.join(savepath,hdf5list), 'a') as f:
            f.write(filename + '\n')

if __name__=='__main__':
    w=39
    h=39
    source_path = '/media/crw/MyBook/MyDataset/FacePoint/test39X39/'
    save_path = '/media/crw/MyBook/TrainData/HDF5/FacePoint/10000_39X39/test/'
    hdf5list='/media/crw/MyBook/TrainData/HDF5/FacePoint/10000_39X39/test/test.txt'
    filelist = '/media/crw/MyBook/MyDataset/FacePoint/test39X39/test.list'
    convert(source_path,filelist,save_path,hdf5list,w,h)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86

网络结构

数据处理好之后,就可以开始设计网络结构了。
网络结构采样的是参照[1]的网络结构,相比于其它的大型的网络的特点在于:
1,输入图像小。
2,权值非共享。
这样的网络相比ImageNet 上的那些模型的优点很明显,参数比较少,学习相对快一些,

图片1:是论文中描绘的网络结构:
论文中的网络结构
图片2:是caffe实现的网络结构:
caffe实现的网络结构

需要注意的是,caffe 的master分支是没有local 层的,这个local 层去年(Caffe Local)就已经请求合并,然而由于各种原因却一直未能合入正式的版本。大家可以从上面那个链接里面clone 版本进行实验。

实验结果

正确的结果
success
失败的结果
//update 2015.11.19://由于之前实验人脸镜面对称的时候,没有将左右眼睛和嘴巴的坐标也对换,导致嘴巴和眼睛都不准的情况出现。感谢 @cyq0122 指出

实验分析

用深度学习来做回归任务,很容易出现回归到均值的问题,在人脸关键点检测的任务中,就是检测到的人脸点是所有值的平均值。在上面失败的例子中,两只眼睛和嘴巴预测的都比较靠近。//修正bug 过后就不会出现这个问题了。

这篇文章中,我只是尝试复现人脸关键点检测文章[1]的第一步,后面有时间的话,也会考虑用caffe 复现所有的结果。

要想完全的复现文章的结果,还需要:
1,级联,从粗到细的检测
2,训练多个网络取平均值(各个网络的输入图像块不一样)

代码托管

DeepFace GitHub 托管 欢迎提改进建议~

//update 2015.11.18:添加了数据处理的python文件。

引用

[1] Y. Sun, X. Wang, and X. Tang. Deep Convolutional Network Cascade for Facial Point Detection. In Proceedings of IEEE Conference on Computer Vision and Pattern Recognition (CVPR), 2013. 

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多