一、背景
人脸表情识别网上已有很多教程,大多基于fer2013数据集展开的。现在的问题就在于fer2013数据集的数量太少,表情的区分度不够明显,大部分基于此数据集的模型,其识别精度仅有70%左右。
因此我想自己从零开始制作人脸表情,而且是非常夸张,有趣的人脸表情,用于后续的表情识别实验。这一篇仅仅介绍如何在没有任何数据的情况下,从零开始制作人脸表情数据集。
二、数据获取
首先没有任何数据的情况下,该如何开始数据的获取呢?其实就两种思路:一种是自己采集,例如用摄像头抓帧,另一种是爬虫爬取数据。实验采用爬虫爬取百度图片中的数据。
然后自己设计自己需要的表情,我自己设置了10类。这里以“吃惊”表情为例,在百度图片搜索中可以看到:
有一部分人脸数据,当然还有很多非人脸数据,这里我们先不管那么多,直接全部爬取下来,爬取的程序采用:
# 导入需要的库
import requests
import os
import json
# 爬取百度图片,解析页面的函数
def getManyPages(keyword, pages):
'''
参数keyword:要下载的影像关键词
参数pages:需要下载的页面数
'''
params = []
for i in range(30, 30 * pages + 30, 30):
params.append({
'tn': 'resultjson_com',
'ipn': 'rj',
'ct': 201326592,
'is': '',
'fp': 'result',
'queryWord': keyword,
'cl': 2,
'lm': -1,
'ie': 'utf-8',
'oe': 'utf-8',
'adpicid': '',
'st': -1,
'z': '',
'ic': 0,
'word': keyword,
's': '',
'se': '',
'tab': '',
'width': '',
'height': '',
'face': 0,
'istype': 2,
'qc': '',
'nc': 1,
'fr': '',
'pn': i,
'rn': 30,
'gsm': '1e',
'1488942260214': ''
})
url = 'https://image.baidu.com/search/acjson'
urls = []
for i in params:
try:
urls.append(requests.get(url, params=i).json().get('data'))
except json.decoder.JSONDecodeError:
print("解析出错")
return urls
# 下载图片并保存
def getImg(dataList, localPath):
'''
参数datallist:下载图片的地址集
参数localPath:保存下载图片的路径
'''
if not os.path.exists(localPath): # 判断是否存在保存路径,如果不存在就创建
os.mkdir(localPath)
x = 0
for list in dataList:
for i in list:
if i.get('thumbURL') != None:
print('正在下载:%s' % i.get('thumbURL'))
ir = requests.get(i.get('thumbURL'))
open(localPath + '%d.jpg' % x, 'wb').write(ir.content)
x += 1
else:
print('图片链接不存在')
# 根据关键词来下载图片
if __name__ == '__main__':
dataList = getManyPages('吃惊', 20) # 参数1:关键字,参数2:要下载的页数
getImg(dataList, './data/chijing/') # 参数2:指定保存的路径
之前我用这段程序爬取过皮卡丘图像,因此不再多做介绍,详细可以参见:对抗神经网络学习(四)——WGAN+爬虫生成皮卡丘图像(tensorflow实现)。这里爬取好的图像直接保存在根目录下的'./data/chijing/'文件夹中。
同样的思路,可以爬取其余9种表情。
三、人脸裁剪及预处理
爬取完人脸表情之后,我们需要裁剪处图像中的人脸,这里设置裁剪大小为128*128。裁剪过程需要用到opencv的人脸识别工具,即haarcascade_frontalface_alt.xml,关于该文件,可从opencv库的根目录中查找,具体查找方法可以参见我之前的文章:深度学习(一)——deepNN模型实现摄像头实时识别人脸表情(C++和python3.6混合编程)。将该文件复制到project的根目录下,然后裁剪人脸,具体的裁剪程序为:
import os
import cv2
# 读取图像,然后将人脸识别并裁剪出来, 参考https://blog.csdn.net/wangkun1340378/article/details/72457975
def clip_image(input_dir, floder, output_dir):
images = os.listdir(input_dir + floder)
for imagename in images:
imagepath = os.path.join(input_dir + floder, imagename)
img = cv2.imread(imagepath)
path = "haarcascade_frontalface_alt.xml"
hc = cv2.CascadeClassifier(path)
faces = hc.detectMultiScale(img)
i = 1
image_save_name = output_dir + floder + imagename
for face in faces:
imgROI = img[face[1]:face[1] + face[3], face[0]:face[0] + face[2]]
imgROI = cv2.resize(imgROI, (128, 128), interpolation=cv2.INTER_AREA)
cv2.imwrite(image_save_name, imgROI)
i = i + 1
print("the {}th image has been processed".format(i))
def main():
input_dir = "data/"
floder = "chijing/"
output_dir = "output/"
if not os.path.exists(output_dir + floder):
os.makedirs(output_dir + floder)
clip_image(input_dir, floder, output_dir)
if __name__ == '__main__':
main()
如果要将该方法用于其余9种表情,只需自己修改main函数中的相关路径即可。这样裁剪之后的影像中,难免混有一些非人脸图像,只需手动删除即可。这样处理之后的效果为:
四、数据增广
经过以上三步,基本可以得到人脸数据集了,但是有的表情的数据量很少,进行模型训练的时候必然会因为数据不足带来影响,因此还需要进行数据增广处理。
一般地,数据增广处理的方法包括:旋转,镜像,随机裁剪,噪声,变形,颜色变化,对比度拉伸等方法。对于人脸来说,这里所选择的处理方法仅有:镜像,即左右反转;随机裁剪,将128*128影响随机裁剪为120*120,再将其resize成128*128;噪声,添加少量的随机噪声。考虑到人脸数据集的特殊性,其他方法暂时没有选择。
这里直接给出数据增广的代码:
# 参考https://blog.csdn.net/qq_36219202/article/details/78339459
import os
from PIL import Image,ImageEnhance
import skimage
import random
import numpy as np
import cv2
# 随机镜像
def random_mirror(root_path, img_name):
img = Image.open(os.path.join(root_path, img_name))
filp_img = img.transpose(Image.FLIP_LEFT_RIGHT)
filp_img = np.asarray(filp_img, dtype="float32")
return filp_img
# 随机平移
def random_move(root_path, img_name, off):
img = Image.open(os.path.join(root_path, img_name))
offset = img.offset(off, 0)
offset = np.asarray(offset, dtype="float32")
return offset
# # 随机转换
# def random_transform( image, rotation_range, zoom_range, shift_range, random_flip ):
# h,w = image.shape[0:2]
# rotation = numpy.random.uniform( -rotation_range, rotation_range )
# scale = numpy.random.uniform( 1 - zoom_range, 1 + zoom_range )
# tx = numpy.random.uniform( -shift_range, shift_range ) * w
# ty = numpy.random.uniform( -shift_range, shift_range ) * h
# mat = cv2.getRotationMatrix2D( (w//2,h//2), rotation, scale )
# mat[:,2] += (tx,ty)
# result = cv2.warpAffine( image, mat, (w,h), borderMode=cv2.BORDER_REPLICATE )
# if numpy.random.random() < random_flip:
# result = result[:,::-1]
# return result
# # 随机变形
# def random_warp( image ):
# assert image.shape == (256,256,3)
# range_ = numpy.linspace( 128-80, 128+80, 5 )
# mapx = numpy.broadcast_to( range_, (5,5) )
# mapy = mapx.T
#
# mapx = mapx + numpy.random.normal( size=(5,5), scale=5 )
# mapy = mapy + numpy.random.normal( size=(5,5), scale=5 )
#
# interp_mapx = cv2.resize( mapx, (80,80) )[8:72,8:72].astype('float32')
# interp_mapy = cv2.resize( mapy, (80,80) )[8:72,8:72].astype('float32')
#
# warped_image = cv2.remap( image, interp_mapx, interp_mapy, cv2.INTER_LINEAR )
#
# src_points = numpy.stack( [ mapx.ravel(), mapy.ravel() ], axis=-1 )
# dst_points = numpy.mgrid[0:65:16,0:65:16].T.reshape(-1,2)
# mat = umeyama( src_points, dst_points, True )[0:2]
#
# target_image = cv2.warpAffine( image, mat, (64,64) )
#
# return warped_image, target_image
# 随机旋转
def random_rotate(root_path, img_name):
img = Image.open(os.path.join(root_path, img_name))
rotation_img = img.rotate(180)
rotation_img = np.asarray(rotation_img, dtype="float32")
return rotation_img
# 随机裁剪
def random_clip(root_path, floder, imagename):
# 可以使用crop_img = tf.random_crop(img,[280,280,3])
img = cv2.imread(root_path + floder + imagename)
count = 1 # 随机裁剪的数量
while 1:
y = random.randint(1, 8)
x = random.randint(1, 8)
cropImg = img[(y):(y + 120), (x):(x + 120)]
image_save_name = root_path + floder + 'clip' + str(count) + imagename
# BGR2RGB
# cropImg = cv2.cvtColor(cropImg, cv2.COLOR_BGR2RGB)
cropImg = cv2.resize(cropImg, (128, 128))
cv2.imwrite(image_save_name, cropImg)
count += 1
print(count)
if count > 3:
break
# 随机噪声
def random_noise(root_path, img_name):
image = Image.open(os.path.join(root_path, img_name))
im = np.array(image)
means = 0
sigma = 10
r = im[:, :, 0].flatten()
g = im[:, :, 1].flatten()
b = im[:, :, 2].flatten()
# 计算新的像素值
for i in range(im.shape[0] * im.shape[1]):
pr = int(r[i]) + random.gauss(means, sigma)
pg = int(g[i]) + random.gauss(means, sigma)
pb = int(b[i]) + random.gauss(means, sigma)
if (pr < 0):
pr = 0
if (pr > 255):
pr = 255
if (pg < 0):
pg = 0
if (pg > 255):
pg = 255
if (pb < 0):
pb = 0
if (pb > 255):
pb = 255
r[i] = pr
g[i] = pg
b[i] = pb
im[:, :, 0] = r.reshape([im.shape[0], im.shape[1]])
im[:, :, 1] = g.reshape([im.shape[0], im.shape[1]])
im[:, :, 2] = b.reshape([im.shape[0], im.shape[1]])
gaussian_image = Image.fromarray(np.uint8(im))
return gaussian_image
# 随机调整对比度
def random_adj(root_path, img_name):
image = skimage.io.imread(os.path.join(root_path, img_name))
gam = skimage.exposure.adjust_gamma(image, 0.5)
log = skimage.exposure.adjust_log(image)
gam = np.asarray(gam, dtype="float32")
log = np.asarray(log, dtype="float32")
return gam, log
# 运行
def main():
root_dir = "output/"
floder = "chijing/"
images = os.listdir(root_dir + floder)
for imagename in images:
mirror_img = random_mirror(root_dir + floder, imagename)
image_save_name = root_dir + floder + "mirror" + imagename
mirror_img = cv2.cvtColor(mirror_img, cv2.COLOR_BGR2RGB)
cv2.imwrite(image_save_name, mirror_img)
random_clip(root_dir, floder, imagename)
noise_img = random_noise(root_dir + floder, imagename)
noise_img = np.asarray(noise_img, dtype="float32")
image_save_name = root_dir + floder + "noise" + imagename
noise_img = cv2.cvtColor(noise_img, cv2.COLOR_BGR2RGB)
cv2.imwrite(image_save_name, noise_img)
print("image preprocessing")
if __name__ == '__main__':
main()
很多处理方法都是参考网上的代码,只不过我将这些方法整合到了一起。对于其余9种表情,需要设置的仅仅只是main函数种的路径参数。
随机裁剪3张、镜像1张、随机噪声1张的处理结果如下:
最终的“吃惊”表情处理结果:
整个处理之后的数据量还是非常可观的,当然了,如果自己觉得数据量还不够,还可以再进行进一步处理,例如对镜像图像进行随机裁剪,不同程度的噪声影像,以及对比度拉伸等方法。
以上仅仅是一种表情的数据集制作,同理制作其余的表情数据集即可。准备好了数据集之后,即可开始进行模型的训练了~