引言
如果关注Kaggle 机器学习项目的同学,一定很熟悉人脸关键点检测这个任务,在2013 年的时候,ICML举办一个的challgene,现在放在kaggle 上作为 一种最常规kaggle入门任务而存在。
本文的主要目的在于验证深度学习模型在人脸点检测效果,踩踩里面的坑。
任务介绍
人脸关键点检测,也称之为人脸点检测,是在一张已经被人脸检测器检测到的人脸图像中,再进一步检测出五官等关键点的二维坐标信息,以便于后期的人脸对齐(face alignment)任务。
根据不同的任务,需要检测的关键点数目有多有少,有些仅要求检测2只眼睛的坐标位置,有些要求检测眼睛、嘴巴、鼻子的5个坐标位置,还有更多的,68个位置,它包含了五官的轮廓信息。
如图所示:
根据任务,可以把要学习的模型函数表示为:
Y=F(X,W)
其中,X 是输入的人脸图像,W是我们要学习的模型参数,Y∈[(x1,y1),(x2,y2),(x3,y3),(x4,y4),(x5,y5)] 是我们需要检测的人脸点坐标位置。
这是一个典型的回归问题,可以采用最简单的平方误差损失函数,然后用机器学习方法学习这个模型。
Loss=15∑i=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 的master分支是没有local 层的,这个local 层去年(Caffe Local)就已经请求合并,然而由于各种原因却一直未能合入正式的版本。大家可以从上面那个链接里面clone 版本进行实验。
实验结果
正确的结果
失败的结果
//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.
|