Автор статьи: Виктория Ляликова

Мне кажется, что распознавание эмоций на лице, является интересной областью исследования, которая представлена и применяется в различных областях искусственного интеллекта. И, наверное, можно сказать, что среди всех задач компьютерного зрения визуальный анализ эмоций является одной из самых сложных задач и при этом достаточно перспективной, поскольку понимание человеческих эмоций является важным шагом на пути к сильному искусственному интеллекту.

В системе анализа эмоций лица человека можно выделить следующие шаги решения задачи.

  1. Получение изображения (или видео с камеры), обнаружение и локализация человеческого лица. Задача обнаружения лица по-прежнему сложна, и нет гарантии, что все лица будут обнаружены на заданном входном изображении, особенно в неконтролируемых условиях со сложными условиями освещения, разными положениями головы на большом расстоянии и т.д.

  2. Предварительная обработка изображения. Когда лица обнаружены, данные изображения оптимизируются перед их подачей в  классификатор. Предварительная обработка изображения обычно включает в себя несколько подэтапов для нормализации изображения с учетом изменений освещения, уменьшения шума, выполнения сглаживания изображения, коррекции поворота изображения, изменения размера изображения и обрезки изображения.Этот шаг значительно повышает точность обнаружения.

  3. Извлечение важных функций с помощью построенной модели нейронной сети. Как правило классификаторы. используемые для распознавания эмоций, основаны на векторных опорных машинах (SVM) или сверточных нейронных сетях (CNN).

  4. Выполнение классификации эмоций путем присвоения предварительно определенного класса (метки).

Попробуем рассмотреть несколько способов решения задачи распознавания эмоций .

1 способ. Использование библиотеки Face expression recognition (или FER). Данная библиотека является Python библиотекой с исходным открытым кодом. Библиотека работает с версией python 3.6 или выше, версией OpenCV - 3.2 или выше, библиотекой Tensorflow 1.7 или выше. Если хотим извлечь еще и  видео со звуком, тогда необходимы пакеты ffmpeg и moviepy.  Работа программы основана на использовании сверточной нейронной сети веса которой представлены в файле HDF5. При необходимости модель можно переобучить.

MTCNN (Multi-task Cascaded Neural Network) является параметром конструктора. По умолчанию лица обнаруживаются с помощью классификатора OpenCV Haar Cascade (каскады Хаара). Когда установлено значение "True", используется модель MTCNN  для более точного обнаружения лица, а когда установлено значение "Ложь", функция использует классификатор OpenCV Haar Cascade.

Устанавливаем библиотеку FER.

pip install fer

Импортируем необходимые пакеты

from fer import FER

import cv2

import matplotlib.pyplot as plt 

Прочитаем и посмотрим на изображение для анализа

test_img = cv2.imread('D:/woman_happy.png')

plt.imshow(test_img[:,:,::-1]))

Инициализируем конструктор fer() путем присвоения ему классификатора распознавания лиц либо OpenCV Haar Cascade, либо MTCNN

emo_detector = FER(mtcnn=True)

Вызываем функцию обнаружения эмоций данного конструктора, передавая ей входное изображение

captured_emotions = emo_detector.detect_emotions(test_img)

Выводим список зафиксированных эмоций 

captured_emotions

[{'box': array([142,  69, 118, 118]), 'emotions': {'angry': 0.0, 'disgust': 0.0, 'fear': 0.0, 'happy': 1.0, 'sad': 0.0, 'surprise': 0.0, 'neutral': 0.0}}]

С помощью метода top_emotion () можно извлечь наиболее доминирующую тональность изображения и ее точность

dominant_emmotion, emotion_score = emo_detector.top_emotion(test_img)

print(dominant_emmotion, emotion_score)

happy 1

Здесь вопросов нет, девушка на фото улыбается, значит определяется доминирующая эмоция как счастье.

Попробуем другое изображение

{'box': array([647, 187, 246, 246]), 'emotions': {'angry': 0.0, 'disgust': 0.0, 'fear': 0.43, 'happy': 0.0, 'sad': 0.0, 'surprise': 0.57, 'neutral': 0.0}}]

surprise 0.57

Здесь классификатор немного сомневается между эмоциями страха и удивления с точностью 0,43 и 0, 57 соответственно, но побеждает доминирующая эмоция “удивление” с точностью 0,57.

Проанализируем еще несколько фотографий на разные эмоции

Здесь, наверное есть вопросы к классификатору только по 2 изображению. Вряд ли эмоцию, представленную на этом изображении можно классифицировать как счастье. Но заметим, что классификатор не уверен в доминирующей эмоции, так как точность здесь только 51%.

2 способ. Библиотека DeepFace Данная библиотека также является открытой Python библиотекой, которая предоставляет следующие возможности 

  1. Проверка лица на фото (Face Verification). Используется для сравнения лица с другими лицами, чтобы проверить совпадает ли оно.

  2. Распознавание лица (Face Recognition).  Используется для поиска лица в базе данных изображений.

  3. Анализ атрибутов лица человека (Face attribute Analysis). Относится к описанию визуальных свойств изображений лица. Анализ атрибутов лица используется для извлечения таких признаков как пол, возраст, раса, анализ эмоций человека на изображении.

  4. Анализ атрибутов лица человека (Face attribute Analysis) в режиме реального времени. Функция включает в себя проверку, распознавание лица и анализ атрибутов лица в видеопотоке в веб-камеры в реальном времени.

DeepFace поддерживает несколько моделей распознавания лиц, такие как VGG-Face, Google FaceNet, OpenFace, Facebook DeepFace, DeepID, ArcFace и Dlib.По умолчанию используется модель VGG-Face. Библиотека в основном основана на Keras и TensorFlow. Для работы с библиотекой понадобятся пакеты cv2 и DeepFace. 

Устанавливаем библиотеку DeepFace

pip install deepFace

Импортируем необходимые пакеты и загружаем исходное изображение

import cv2

from deepface import DeepFace

import matplotlib.pyplot as plt

  1. Начнем с функции проверки лица на фото (Face Verification).

Создадим функцию, которая на вход принимает изображения, показывает их и с помощью функции DeepFace.verify() проводит проверку лиц на фото. Метод verify() возвращает словарь, главным ключом является verified в котором хранится True в случае сходства и False при различии.

def verify(img1_path, img2_path):
	img1= cv2.imread(img1_path)
	img2= cv2.imread(img2_path)
    
	fig = plt.figure(figsize=(10,7))
	rows=1
	columns=2

	fig.add_subplot(rows,columns,1)
 
	plt.imshow(img1[:,:,::-1])
	plt.axis('off')
	plt.title('First')
	fig.add_subplot(rows,columns,2)
 
	plt.imshow(img2[:,:,::-1])
	plt.axis('off')
	plt.title('Second')
	plt.show()
	output = DeepFace.verify(img1_path,img2_path)
	print(output)
	verification = output['verified']
	if verification:
   	print('They are same')
	else:
   	print('The are not same')

Теперь запустим функцию verify()

verify('D:/Jennifer_Aniston1.jpg','D:/Jennifer_Aniston2.jpg')

Видим, что изображения прошли проверку на идентичность лиц.

Теперь сравним 2 разных лица

verify('D:/Jennifer_Aniston1.jpg','D:/Parker.jpg')  

Здесь видим, что лица отмечены как непохожие.

А вот здесь, представлен один и тот же человек, но за счет разного возраста, цвета волос, фото отмечены как принадлежащие разным людям.

Попробуем использовать другие модели, такие как Google Facenet и Facebook DeepFace. 

output = DeepFace.verify(img1_path,img2_path, “Facenet”)

output = DeepFace.verify(img1_path,img2_path,”DeepFace”)

Данные модели определили лица как принадлежащие одному человеку.

  1. Теперь перейдем к функции поиска лиц в базе DeepFace.find() (Face Recognition). Метод find() принимает два параметра: путь к изображению, которое проверяем и путь к папке с базой изображений человека. Метод возвращает дата фрейм с информацией о совпадении или отсутствия совпадений. Возьмем фотографию Эмилии Кларк и будем в базе искать ее фотографии

recognition = DeepFace.find('D:/Klark3.jpg', 'D:/Face')

Видим, что в результаты сравнения также попала фотография, принадлежащая Дженнифер Энистон.

  1. Теперь перейдем непосредственно к анализу эмоций (Face attribute Analysis). Для анализа атрибутов лица человека используется функция DeepFace.analyze(), которая принимает один параметр - путь к изображению.

img = 'D:/Jennifer_Aniston8.jpeg'
imageFace = cv2.imread(img_path)
plt.imshow(imageFace[:,:,::-1])
analyze = DeepFace.analyze(img)
analyze

Классификатор определил, что на фотографии женщина 24 лет белой расы, выражающая эмоции счастья.

Также можно явно указать какие именно характеристики нас интересуют и выделить доминирующий признак. Возьмем, например, такое изображение.

analysis = DeepFace.analyze(img, actions = ['emotion'])

Доминирующая эмоция: удивление. 

Проанализируем еще несколько изображений

Интересный результат. В зависимости от эмоций на лице меняется также определяемый возраст, раса и даже пол.

3 способ. Вместо использования различных пакетов попробуем обучить собственную модель распознаванию эмоций на лице человека.

Для обучения модели будем использовать набор данных FER, который состоит из изображений лиц в градациях серого размера 48х48 пикселей.Лица находятся более или менее по центру и занимают примерно одинаковое количество места на каждом изображении. Каждое лицо на основе отображаемых эмоций может быть классифицировано в одну из семи категорий (0 - злость, 1 - отвращение, 2 - страх, 3- радость, 4 - грусть, 5 - удивление, 6 - нейтрально). Обучающий набор состоит из 28 709 примеров, а тестовый набор состоит из 3 589 примеров. Попробуем обучить нашу модель 5 эмоциям (злость (angry), радость (happy), грусть (sad), удивление (surprise), нейтрально (neutral)).

импортируем необходимые библиотеки

import imutils
import cv2
import os
import glob
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.layers import Dense, Input, Dropout,Flatten, Conv2D
from tensorflow.keras.layers import Activation, MaxPool2D
from keras.layers import BatchNormalization
from keras_preprocessing.image import img_to_arr
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras.callbacks import Callback, ModelCheckpoint,EarlyStopping, ReduceLROnPlateau

Считываем обучающие и тестовые наборы из файлов и переводим матричные представления изображений в вестор [0,1], разделив каждое значение пикселя на 255.

train_folder = "*****/FaceEmotion/train/"
X_train = []
y_train = []
labels = 0
for dir in os.listdir(train_folder):
	path = train_folder+dir +'/'
	emotions.append(dir)
	for filename in os.listdir(path):
    	img = cv2.imread(path + filename)   
    	gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
    	img = cv2.resize(gray,(shape))
    	image_norm = np.array(img)/255   
    	X_train.append(image_norm)
    	y_train.append(labels)
	labels+=1

Тестовые наборы считываются аналогично, поэтому код можно не приводить

Примеры изображений

Модель нейронной сети имеет 5 блоков с несколькими сверточными слоями (Conv2D) и слоями максимального объединения (MaxPool2D), 3 полносвязных слоя (Dense) и выходной слой с 5 выходами в соответствии с количеством классов эмоций. Выходной слой имеет активационную функцию softmax, так как она возвращает распределение вероятностей по целевым классам в задаче многоклассовой классификации.  Во всех сверточных слоях используется активационная функция elu, так как она позволяет избежать проблем, связанных с линейной активационной функцией Relu.

ELU(x) =   \begin{cases} x   & \quad \text{if } x>0 \\  \alpha(e^x-1) & \quad \text{if } x<0 \end{cases}

Также используются слои нормализации или батч-нормализация (BatchNormalization) и слои Dropouts, чтобы избежать переобучения. Наша модель будет иметь 5 282 533 настраиваемых параметра.

model = Sequential()
#block 1
model.add(Conv2D(32, (3, 3), padding = 'same', kernel_initializer='he_normal',activation = 'elu', input_shape = (img_r, img_r, 1)))
model.add(BatchNormalization())
model.add(Conv2D(32, (3, 3), padding = 'same', kernel_initializer='he_normal',activation = 'elu'))
model.add(BatchNormalization())
model.add(MaxPool2D( (2,2), padding = 'same'))
model.add(Dropout(0.5))
#block 2
model.add(Conv2D(64, (3, 3), padding = 'same',kernel_initializer='he_normal', activation = 'elu'))
model.add(BatchNormalization())
model.add(Conv2D(64, (3, 3), padding = 'same', kernel_initializer='he_normal',activation = 'elu'))
model.add(BatchNormalization())
model.add(MaxPool2D( (2,2), padding = 'same'))
model.add(Dropout(0.5))
#block 3
model.add(Conv2D(128, (3, 3), padding = 'same', kernel_initializer='he_normal', activation = 'elu'))
model.add(BatchNormalization())
model.add(Conv2D(128, (3, 3), padding = 'same', kernel_initializer='he_normal', activation = 'elu'))
model.add(BatchNormalization())
model.add(MaxPool2D( (2,2), padding = 'same'))
model.add(Dropout(0.5))
#block 4
model.add(Conv2D(256, (3, 3), padding = 'same', kernel_initializer='he_normal', activation = 'elu'))
model.add(BatchNormalization())
model.add(Conv2D(256, (3, 3), padding = 'same', kernel_initializer='he_normal', activation = 'elu'))
model.add(BatchNormalization())
model.add(MaxPool2D( (2,2), padding = 'same'))
model.add(Dropout(0.5))
#block 5
model.add(Conv2D(512, (3, 3), padding = 'same', kernel_initializer='he_normal', activation = 'elu'))
model.add(BatchNormalization())
model.add(Conv2D(512, (3, 3), padding = 'same', kernel_initializer='he_normal', activation = 'elu'))
model.add(BatchNormalization())
model.add(MaxPool2D( (2,2), padding = 'same'))
model.add(Dropout(0.5))
#Block6
model.add(Flatten())
model.add(Dense(256, kernel_initializer='he_normal', activation='elu'))
model.add(BatchNormalization())
model.add(Dropout(0.5))
#block7
model.add(Dense(128, kernel_initializer='he_normal', activation='elu'))
model.add(BatchNormalization())
model.add(Dropout(0.5))
#block8
model.add(Dense(64, kernel_initializer='he_normal', activation='elu'))
model.add(BatchNormalization())
model.add(Dropout(0.5))
#block8
model.add(Dense(5, activation = 'softmax'))

Используется метод ModelCheckpoint для сохранения весов модели, основываясь на потерях на этапе проверки и метод ReduceROnPlateau для уменьшения скорости обучения, если будет происходит стагнация в обучении.

checkpoint = ModelCheckpoint('D:/Emotion_5.h5',
                         	monitor='val_loss',
                         	mode='min',
                         	save_best_only=True,
                         	verbose=1)

reduce_lr = ReduceLROnPlateau(monitor='val_loss',
                          	factor=0.2,
                          	patience=5,
                          	verbose=1,
                          	min_lr=1e-7)
callbacks = [checkpoint,reduce_lr]
                   	verbose=1)

Наконец, компилируем и подгоняем модель, используя разреженную категориальную перекрестную энтропию (sparse_categorical_crossentropy), так как у нас задача многоклассовой классификации и оптимизатор Адама.

model.compile(optimizer = 'adam', loss = 'sparse_categorical_crossentropy', metrics = ['accuracy'] )

history = model.fit(x_train, y_train, batch_size=32, epochs = 23, validation_data = (x_test, y_test),callbacks=callbacks,use_multiprocessing=True).

Посмотрим на графики точности и потери  на этапах проверки и обучения

plt.plot(history.history['accuracy'])
plt.plot(history.history['val_accuracy'])
plt.title('Точность на этапах проверки и обучения')
plt.ylabel('Точность')
plt.xlabel('Эпохи')
plt.legend(['Точность на этапе обучения', 'Точность на этапе проверки'], loc='upper left')
plt.show()

plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('Потери на этапах проверки и обучения')
plt.ylabel('Потери')
plt.xlabel('Эпохи')
plt.legend(['Потери на этапе обучения', 'Потери на этапе проверки'], loc='upper left')
plt.show()

Теперь после обучения посмотрим как работает наша модель. Для этого будем использовать каскады Хаара из библиотеки OpenCV, чтобы обнаружить лицо и нарисовать вокруг него ограничивающую рамку. 

class_labels = ['Angry','Happy','Neutral','Sad','Surprise']
# читаем изображения из папки
images = [cv2.imread(file) for file in glob.glob(r"D:\klark\*")]
trained_face_data = cv2.CascadeClassifier(r'D:\haarcascade_frontalface_default.xml')

fig, ax = plt.subplots(nrows=1, ncols=4, figsize=(15,15))
font = cv2.FONT_HERSHEY_TRIPLEX
#в цикле просматриваем все изображения
for i in range(len(images)):
           #переводим изображение в градации серого
	grayscaled_img = cv2.cvtColor(images[i], cv2.COLOR_BGR2GRAY)
           # определяем координаты лица
	face_coordinates = trained_face_data.detectMultiScale(grayscaled_img)
    
	for (x,y,w,h) in face_coordinates:
            # рисуем рамку вокруг лица
    	cv2.rectangle(images[i], (x,y), (x+w,y+h), (0,255,0),15)

            # изменяем размер изображения и нормируем значения
    	roi_gray = grayscaled_img[y:y+h,x:x+w]
    	roi_gray = cv2.resize(roi_gray,(48,48),interpolation=cv2.INTER_AREA)

    	if np.sum([roi_gray])!=0:
                roi = roi_gray.astype('float')/255.0
        	    roi = img_to_array(roi)
        	    roi = np.expand_dims(roi,axis=0)

                # делаем прогноз и определяем класс
        	    preds = model.predict(roi)[0]
         	    label=class_labels[preds.argmax()]
        	    label_position = (x,y)
                # над рамкой пишем эмоцию, предсказанную классификатором
        	    cv2.putText(images[i],label,label_position,font,3,(0,0,255),7)
    	else:
        	    cv2.putText(images[i],'No Face Found',(30,60),font,2,(0,0,255),3)    
   	 
	# отображаем изображения
	ax[i].axis("off")
	ax[i].imshow(images[i][:,:,::-1])

 

Эмоции, предсказанные классификатором : ['Sad', 'Surprise', 'Happy', 'Neutral'].

В, принципе, полученные результаты неплохи и здесь есть над чем поработать в дальнейшем. Например, сбалансировать обучающий набор данных, изменить структуру нейронной сети, увеличив количество слоев, или изменив параметры сверточных слоев и т.д.

В завершение приглашаю всех желающих на бесплатный урок от OTUS, где поговорим о том какие подходы к ансамблированию сегодня существуют в машинном обучении. Как устроены такие популярные техники ансамблирования как Bagging, Random Forest и Gradient Boosting. Когда и как их стоит применять для решения ML-задач.

Комментарии (2)


  1. jobless
    00.00.0000 00:00

    Экскурс в историю и басня от туда же, подтверждение которой я сколько не пытался, не нахожу. Осенью 1988 года в Переславле - Залесском была конференция по экспертным системам. Кто то из докладчиков рассказывал о неком американце с типично американскими именем и фамилией - Иван Чернов. Так вот он якобы предложил способ показывать обобщённое состояние сложной системы (например АЭС) оператору контроля не в виде массы стрелочек на линейных и круговых индикаторах, а в виде мимики человеческого лица. И назвал он это МОРДОГРАФИЕЙ. Я очень хорошо помню как это слушал, сам бы такое не придумал точно, но попытки найти в сети подтверждение существования этой самой мордографии не увенчались успехом.


  1. vagon333
    00.00.0000 00:00

    Была статья на хабре, где мимика лица разбивалась на мышечные группы и анализ имел более академический подход, с определением шаблонов эмоций, позволяющих отметить даже ложные эмоции, типа ложных улыбок.