1. 程式人生 > >從零開始製作人臉表情的資料集

從零開始製作人臉表情的資料集

一、背景

人臉表情識別網上已有很多教程,大多基於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張的處理結果如下:

最終的“吃驚”表情處理結果:

整個處理之後的資料量還是非常可觀的,當然了,如果自己覺得資料量還不夠,還可以再進行進一步處理,例如對映象影象進行隨機裁剪,不同程度的噪聲影像,以及對比度拉伸等方法。

以上僅僅是一種表情的資料集製作,同理製作其餘的表情資料集即可。準備好了資料集之後,即可開始進行模型的訓練了~