人臉表情識別系統介紹——上篇(python實現,含UI介面及完整程式碼)

思緒無限發表於2022-04-19
功能演示動圖

摘要:這篇博文介紹基於深度卷積神經網路實現的人臉表情識別系統,系統程式由Keras, OpenCv, PyQt5的庫實現,訓練測試集採用fer2013表情庫。如圖系統可通過攝像頭獲取實時畫面並識別其中的人臉表情,也可以通過讀取圖片識別,本文提供完整的程式檔案並詳細介紹其實現過程。博文要點如下:

點選跳轉至文末博文涉及的全部檔案下載頁

下載連結:博主在麵包多網站上的完整資源下載頁

人臉表情識別介紹與演示視訊連結https://www.bilibili.com/video/BV18C4y1H7mH/

功能演示動圖

1. 前言

    在這個人工智慧成為超級大熱門的時代,人臉表情識別已成為其中的一項研究熱點,而卷積神經網路、深度信念網路和多層感知器等相關演算法在人臉面部表情識別領域的運用最為廣泛。面部的表情中包含了太多的資訊,輕微的表情變化都會反映出人心理的變化,可想而知如果機器能敏銳地識別人臉中表達的情感該是多麼令人興奮的事。

    學習和研究了挺久的深度學習,偶然看到IEEE上面一篇質量很高的文章,裡面介紹的是利用深度神經網路實現的面部表情識別,研讀下來讓我深受啟發。於是自己動手做了這個專案,如今SCI論文已投稿,這裡特此將前期工作作個總結,希望能給類似工作的朋友帶來一點幫助。由於論文尚未公開,這裡使用的是已有的模型——如今CNN的主流框架之mini_XCEPTION,該模型效能也已是不錯的了,論文中改進的更高效能模型尚不便給出,後面會分享給大家,敬請關注。


2. 表情識別資料集

    目前,現有的公開的人臉表情資料集比較少,並且數量級比較小。比較有名的廣泛用於人臉表情識別系統的資料集Extended Cohn-Kanada (CK+)是由P.Lucy收集的。CK+資料集包含123 個物件的327 個被標記的表情圖片序列,共分為正常、生氣、蔑視、厭惡、恐懼、開心和傷心七種表情。對於每一個圖片序列,只有最後一幀被提供了表情標籤,所以共有327 個影像被標記。為了增加資料,我們把每個視訊序列的最後三幀影像作為訓練樣本。這樣CK+資料總共被標記的有981 張圖片。這個資料庫是人臉表情識別中比較流行的一個資料庫,很多文章都會用到這個資料做測試,可通過下面的連結下載。
官網連結:The Extended Cohn-Kanade Dataset(CK+)
網盤連結:百度網盤下載(提取碼:8r15

功能演示動圖

    KaggleKaggle人臉表情分析比賽提供的一個資料集。該資料集含28709 張訓練樣本,3859 張驗證資料集和3859 張測試樣本,共35887 張包含生氣、厭惡、恐懼、高興、悲傷、驚訝和正常七種類別的影像,影像解析度為48×48。該資料集中的影像大都在平面和非平面上有旋轉,並且很多影像都有手、頭髮和圍巾等的遮擋物的遮擋。該資料庫是2013年Kaggle比賽的資料,由於這個資料庫大多是從網路爬蟲下載的,存在一定的誤差性。這個資料庫的人為準確率是65%±5%
官網連結:FER2013
網盤連結:百度網盤下載(提取碼:t7xj

功能演示動圖

    由於FER2013資料集資料更加齊全,同時更加符合實際生活的場景,所以這裡主要選取FER2013訓練和測試模型。為了防止網路過快地過擬合,可以人為的做一些影像變換,例如翻轉,旋轉,切割等。上述操作稱為資料增強。資料操作還有另一大好處是擴大資料庫的資料量,使得訓練的網路魯棒性更強。下載資料集儲存在fer2013的資料夾下,為了對資料集進行處理,採用如下程式碼載入和進行圖片預處理:

import pandas as pd
import cv2
import numpy as np

dataset_path = 'fer2013/fer2013/fer2013.csv' # 檔案儲存位置
image_size=(48,48) # 圖片大小

# 載入資料
def load_fer2013():
        data = pd.read_csv(dataset_path)
        pixels = data['pixels'].tolist()
        width, height = 48, 48
        faces = []
        for pixel_sequence in pixels:
            face = [int(pixel) for pixel in pixel_sequence.split(' ')]
            face = np.asarray(face).reshape(width, height)
            face = cv2.resize(face.astype('uint8'),image_size)
            faces.append(face.astype('float32'))
        faces = np.asarray(faces)
        faces = np.expand_dims(faces, -1)
        emotions = pd.get_dummies(data['emotion']).as_matrix()
        return faces, emotions

# 將資料歸一化
def preprocess_input(x, v2=True):
    x = x.astype('float32')
    x = x / 255.0
    if v2:
        x = x - 0.5
        x = x * 2.0
    return x

    載入資料後將資料集劃分為訓練集和測試集,在程式中呼叫上面的函式程式碼如下:

from load_and_process import load_fer2013
from load_and_process import preprocess_input
from sklearn.model_selection import train_test_split

# 載入資料集
faces, emotions = load_fer2013()
faces = preprocess_input(faces)
num_samples, num_classes = emotions.shape

# 劃分訓練、測試集
xtrain, xtest,ytrain,ytest = train_test_split(faces, emotions,test_size=0.2,shuffle=True)

3. 搭建表情識別的模型

    接下來就是搭建表情識別的模型了,這裡用到的是CNN的主流框架之mini_XCEPTIONXCEPTIONGoogleInception後提出的對Inception v3的另一種改進,主要是採用深度可分離的卷積(depthwise separable convolution)來替換原來Inception v3中的卷積操作。XCEPTION的網路結構在ImageNet資料集(Inception v3的設計解決目標)上略優於Inception v3,並且在包含3.5億個影像甚至更大的影像分類資料集上明顯優於Inception v3,而兩個結構保持了相同數目的引數,效能增益來自於更加有效地使用模型引數,詳細可參考論文:Xception: Deep Learning with Depthwise Separable Convolutions,論文Real-time Convolutional Neural Networks for Emotion and Gender Classification等。

功能演示動圖

    既然這樣的網路能獲得更好結果又是主流,那當然有必要作為對比演算法實現以下了,這裡博主模型這部分的程式碼引用了GitHub:https://github.com/oarriaga/face_classification中的模型(其他地方也能找到這個模型的類似程式碼),模型框圖如上圖所示,其程式碼如下:

def mini_XCEPTION(input_shape, num_classes, l2_regularization=0.01):
    regularization = l2(l2_regularization)

    # base
    img_input = Input(input_shape)
    x = Conv2D(8, (3, 3), strides=(1, 1), kernel_regularizer=regularization,
                                            use_bias=False)(img_input)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    x = Conv2D(8, (3, 3), strides=(1, 1), kernel_regularizer=regularization,
                                            use_bias=False)(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)

    # module 1
    residual = Conv2D(16, (1, 1), strides=(2, 2),
                      padding='same', use_bias=False)(x)
    residual = BatchNormalization()(residual)

    x = SeparableConv2D(16, (3, 3), padding='same',
                        kernel_regularizer=regularization,
                        use_bias=False)(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    x = SeparableConv2D(16, (3, 3), padding='same',
                        kernel_regularizer=regularization,
                        use_bias=False)(x)
    x = BatchNormalization()(x)

    x = MaxPooling2D((3, 3), strides=(2, 2), padding='same')(x)
    x = layers.add([x, residual])

    # module 2
    residual = Conv2D(32, (1, 1), strides=(2, 2),
                      padding='same', use_bias=False)(x)
    residual = BatchNormalization()(residual)

    x = SeparableConv2D(32, (3, 3), padding='same',
                        kernel_regularizer=regularization,
                        use_bias=False)(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    x = SeparableConv2D(32, (3, 3), padding='same',
                        kernel_regularizer=regularization,
                        use_bias=False)(x)
    x = BatchNormalization()(x)

    x = MaxPooling2D((3, 3), strides=(2, 2), padding='same')(x)
    x = layers.add([x, residual])

    # module 3
    residual = Conv2D(64, (1, 1), strides=(2, 2),
                      padding='same', use_bias=False)(x)
    residual = BatchNormalization()(residual)

    x = SeparableConv2D(64, (3, 3), padding='same',
                        kernel_regularizer=regularization,
                        use_bias=False)(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    x = SeparableConv2D(64, (3, 3), padding='same',
                        kernel_regularizer=regularization,
                        use_bias=False)(x)
    x = BatchNormalization()(x)

    x = MaxPooling2D((3, 3), strides=(2, 2), padding='same')(x)
    x = layers.add([x, residual])

    # module 4
    residual = Conv2D(128, (1, 1), strides=(2, 2),
                      padding='same', use_bias=False)(x)
    residual = BatchNormalization()(residual)

    x = SeparableConv2D(128, (3, 3), padding='same',
                        kernel_regularizer=regularization,
                        use_bias=False)(x)
    x = BatchNormalization()(x)
    x = Activation('relu')(x)
    x = SeparableConv2D(128, (3, 3), padding='same',
                        kernel_regularizer=regularization,
                        use_bias=False)(x)
    x = BatchNormalization()(x)

    x = MaxPooling2D((3, 3), strides=(2, 2), padding='same')(x)
    x = layers.add([x, residual])

    x = Conv2D(num_classes, (3, 3),
            #kernel_regularizer=regularization,
            padding='same')(x)
    x = GlobalAveragePooling2D()(x)
    output = Activation('softmax',name='predictions')(x)

    model = Model(img_input, output)
    return model

4. 資料增強的批量訓練

    神經網路的訓練需要大量的資料,資料的量決定了網路模型可以達到的高度,網路模型儘量地逼近這個高度。然而對於人臉表情的資料來說,都只存在少量的資料Extended Cohn-Kanada (CK+)的資料量是遠遠不夠的,並且CK+多是比較誇張的資料。Kaggle Fer2013資料集也不過只有3萬多資料量,而且有很多遮擋、角度等外界影響因素。既然收集資料要花費很大的人力物力,那麼我們就用技術解決這個問題,為避免重複開發首先還是看看有沒有寫好的庫。博主又通讀了遍Keras官方文件,其中ImageDataGenerator的圖片生成器就可完成這一目標。

為了儘量利用我們有限的訓練資料,我們將通過一系列隨機變換堆資料進行提升,這樣我們的模型將看不到任何兩張完全相同的圖片,這有利於我們抑制過擬合,使得模型的泛化能力更好。在Keras中,這個步驟可以通過keras.preprocessing.image.ImageGenerator來實現,這個類使你可以:在訓練過程中,設定要施行的隨機變換通過.flow或.flow_from_directory(directory)方法例項化一個針對影像batch的生成器,這些生成器可以被用作keras模型相關方法的輸入,如fit_generator,evaluate_generatorpredict_generator。——Keras官方文件

    ImageDataGenerator()是一個圖片生成器,同時也可以在batch中對資料進行增強,擴充資料集大小(比如進行旋轉,變形,歸一化等),增強模型的泛化能力。結合前面的模型和資料訓練部分的程式碼如下:

"""
Description: 訓練人臉表情識別程式
"""

from keras.callbacks import CSVLogger, ModelCheckpoint, EarlyStopping
from keras.callbacks import ReduceLROnPlateau
from keras.preprocessing.image import ImageDataGenerator
from load_and_process import load_fer2013
from load_and_process import preprocess_input
from models.cnn import mini_XCEPTION
from sklearn.model_selection import train_test_split

# 引數
batch_size = 32
num_epochs = 10000
input_shape = (48, 48, 1)
validation_split = .2
verbose = 1
num_classes = 7
patience = 50
base_path = 'models/'

# 構建模型
model = mini_XCEPTION(input_shape, num_classes)
model.compile(optimizer='adam', # 優化器採用adam
              loss='categorical_crossentropy', # 多分類的對數損失函式
              metrics=['accuracy'])
model.summary()

# 定義回撥函式 Callbacks 用於訓練過程
log_file_path = base_path + '_emotion_training.log'
csv_logger = CSVLogger(log_file_path, append=False)
early_stop = EarlyStopping('val_loss', patience=patience)
reduce_lr = ReduceLROnPlateau('val_loss', factor=0.1,
                              patience=int(patience/4),
                              verbose=1)
# 模型位置及命名
trained_models_path = base_path + '_mini_XCEPTION'
model_names = trained_models_path + '.{epoch:02d}-{val_acc:.2f}.hdf5'

# 定義模型權重位置、命名等
model_checkpoint = ModelCheckpoint(model_names,
                                   'val_loss', verbose=1,
                                    save_best_only=True)
callbacks = [model_checkpoint, csv_logger, early_stop, reduce_lr]

# 載入資料集
faces, emotions = load_fer2013()
faces = preprocess_input(faces)
num_samples, num_classes = emotions.shape

# 劃分訓練、測試集
xtrain, xtest,ytrain,ytest = train_test_split(faces, emotions,test_size=0.2,shuffle=True)

# 圖片產生器,在批量中對資料進行增強,擴充資料集大小
data_generator = ImageDataGenerator(
                        featurewise_center=False,
                        featurewise_std_normalization=False,
                        rotation_range=10,
                        width_shift_range=0.1,
                        height_shift_range=0.1,
                        zoom_range=.1,
                        horizontal_flip=True)

# 利用資料增強進行訓練
model.fit_generator(data_generator.flow(xtrain, ytrain, batch_size),
                        steps_per_epoch=len(xtrain) / batch_size,
                        epochs=num_epochs,
                        verbose=1, callbacks=callbacks,
                        validation_data=(xtest,ytest))

    以上程式碼中設定了訓練時的結果輸出,在訓練結束後會將訓練的模型儲存為hdf5檔案到自己指定的資料夾下,由於資料量大模型的訓練時間會比較長,建議使用GPU加速。訓練結束後測試得到混淆矩陣如下:

功能演示動圖

    訓練的模型綜合在FER2013資料集上的分類準確率為66%,後續調整之後達到了70%,算是中等偏上水平,其實並非模型不好而是在資料預處理、超引數的選取上有很大的可提升空間,當然也可使用其他的模型,譬如可參考論文:Extended deep neural network for facial emotion recognition,大家可自行研究,這裡就不多介紹了。


5. 系統UI介面的實現

    上面的模型訓練好了,但對於我們來說它的作用就只是知道了其準確率還行,其實深度學習的目的最重要還是應用,是時候用上面的模型做點酷酷的東西了。可不可以用上面的模型識別下自己表達的情緒呢?不如做個系統調取攝像頭對實時畫面中的表情進行識別並顯示識別結果,既能視覺化的檢測模型的實用效能,同時使得整個專案生動有趣激發自己的創造性,當你向別人介紹你的專案時也顯得高大上。這裡採用PyQt5進行設計,首先看一下最後的效果圖,執行後的介面如下:

功能演示動圖

    設計功能:一、可選擇模型檔案後基於該模型進行識別;二、開啟攝像頭識別實時畫面中的人臉表情;三、選擇一張人臉圖片,對其中的表情進行識別。選擇一張圖片測試識別效果,如下圖所示:

功能演示動圖

    博主對UI介面的要求是可以簡單但顏值必須高,必須高,實用簡約高顏值是我奉行的標準,以上的介面幾經修改才有了上面的效果。當然博主的目的並不單純的想秀,而是藉此做一個測試模型的系統,可以選擇模型、訓練測試集等以便介面化地對後面的模型進行各種測試評估,生成用於論文的特定結果資料圖或表格等,這個測試系統後面有機會分享給大家。

    系統UI介面的實現這部分又設計PyQt5的許多內容,在這一篇博文中介紹恐怕尾大不掉,效果也不好,所以更多的細節內容將在後面的博文中介紹,敬請期待!有需要的朋友可通過下面的連結下載這部分的檔案。


下載連結
    若您想獲得博文中涉及的實現完整全部程式檔案(包括資料集,py, UI檔案等,如下圖),這裡已打包上傳至博主的麵包多下載資源中。檔案下載連結如下:

功能演示動圖

資料連結:訓練用到的資料集(提取碼:t7xj

    本資源已上傳至麵包多網站,可以點選以下連結獲取,已將資料集同時打包到裡面,點選即可執行,完整檔案下載連結如下:

完整資源下載連結博主在麵包多網站上的完整資源下載頁

【執行程式須知】
在這裡插入圖片描述

    要安裝的庫如上圖(以上是博主安裝的版本),如您想直接執行介面程式,只需在下載連結1中的檔案後,執行runMain.py程式。

    如您想重新訓練模型,下載連結1中的檔案後,執行前請下載連結2中的資料集解壓到的csv檔案放到 fer2013\fer2013 的資料夾下,執行train_emotion_classifier.py程式即可重新訓練。

詳細安裝教程人臉表情識別系統介紹——離線環境配置篇

公眾號獲取
    本人微信公眾號已建立,掃描以下二維碼並關注公眾號“AI技術研究與分享”,後臺回覆“ER20190609”獲取。

功能演示動圖

5. 結束語

    由於博主能力有限,博文中提及的方法與程式碼即使經過測試,也難免會有疏漏之處。希望您能熱心指出其中的錯誤,以便下次修改時能以一個更完美更嚴謹的樣子,呈現在大家面前。同時如果有更好的實現方法也請您不吝賜教。

    大家的點贊和關注是博主最大的動力,博主所有博文中的程式碼檔案都可分享給您,如果您想要獲取博文中的完整程式碼檔案,可通過C幣或積分下載,沒有C幣或積分的朋友可在關注、點贊博文後提供郵箱,我會在第一時間傳送給您。博主後面會有更多的分享,敬請關注哦!

參考文獻:
[1] Chollet F. Xception: Deep learning with depthwise separable convolutions[C]//Proceedings of the IEEE conference on computer vision and pattern recognition. 2017: 1251-1258.
[2] Arriaga O, Valdenegro-Toro M, Plöger P. Real-time convolutional neural networks for emotion and gender classification[J]. arXiv preprint arXiv:1710.07557, 2017.
[3] Jain D K, Shamsolmoali P, Sehdev P. Extended deep neural network for facial emotion recognition[J]. Pattern Recognition Letters, 2019, 120: 69-74.

相關文章