从零开始制作人脸表情的数据集

时间:2024-03-27 10:55:48

一、背景

人脸表情识别网上已有很多教程,大多基于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张的处理结果如下:

从零开始制作人脸表情的数据集

最终的“吃惊”表情处理结果:

从零开始制作人脸表情的数据集

整个处理之后的数据量还是非常可观的,当然了,如果自己觉得数据量还不够,还可以再进行进一步处理,例如对镜像图像进行随机裁剪,不同程度的噪声影像,以及对比度拉伸等方法。

以上仅仅是一种表情的数据集制作,同理制作其余的表情数据集即可。准备好了数据集之后,即可开始进行模型的训练了~