В современном мире наши самые глубокие эмоции часто выражаются не словами, а тонкими нюансами выражения лица. Эти мгновенные вспышки эмоций, будь то краткая улыбка или краткий удивленный взгляд, часто передают гораздо больше, чем слова. Но что, если бы мы могли использовать возможности искусственного интеллекта (ИИ) для исследования этого царства невысказанных эмоций? Что, если бы мы могли научить машину понимать и интерпретировать человеческие эмоции, просто анализируя выражение лица? Добро пожаловать в увлекательное путешествие по распознаванию эмоций с помощью ИИ.

В этой записной книжке мы погружаемся в сложный процесс распознавания эмоций из видеоданных с использованием ИИ, управляемого захватывающим сочетанием библиотек Python и преобразующих моделей ИИ. Нашим гидом в этом путешествии является модель ViT-Face-Expression от Hugging Face, предварительно обученная модель на основе трансформера, специально разработанная для задач обнаружения эмоций.

Мы будем использовать широкий спектр библиотек Python, чтобы ориентироваться в этом увлекательном ландшафте. Мы будем использовать numpy и pandasдля обработки данных, matplotlib и seaborn для визуализации данных и moviepy для обработки видео. Для обработки изображений мы воспользуемся возможностями Python Imaging Library (PIL). Мы будем использовать MTCNN facenet_pytorch для точного распознавания лиц и трансформеры для использования передовой модели ViT-Face-Expression.

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

Импорт библиотек

import os

# Set cache directories for XDG and Hugging Face Hub
os.environ['XDG_CACHE_HOME'] = '/home/msds2023/jlegara/.cache'
os.environ['HUGGINGFACE_HUB_CACHE'] = '/home/msds2023/jlegara/.cache'

import torch

# Set device to GPU if available, otherwise use CPU
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
print('Running on device: {}'.format(device))

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
import seaborn as sns
from tqdm.notebook import tqdm

from moviepy.editor import VideoFileClip, ImageSequenceClip

import torch
from facenet_pytorch import (MTCNN)

from transformers import (AutoFeatureExtractor,
                          AutoModelForImageClassification,
                          AutoConfig)
                             
from PIL import Image, ImageDraw

Загрузить видеоданные

# Load your video
scene = 'White Chicks - short.mp4'
clip = VideoFileClip(scene)

# Save video frames per second
vid_fps = clip.fps

# Get the video (as frames)
video = clip.without_audio()
video_data = np.array(list(video.iter_frames()))

Обнаружение однокадровых эмоций

Функция detect_emotions(image) является центральной частью нашего процесса обнаружения эмоций для однокадрового обнаружения эмоций. Он принимает в качестве входных данных один кадр изображения (в формате PIL Image) и выполняет несколько операций, чтобы в конечном итоге отобразить обнаруженное лицо и гистограмму соответствующих вероятностей эмоций.

Во-первых, функция создает копию изображения для рисования, чтобы убедиться, что мы не перезаписываем исходное изображение. Затем он использует модель MTCNN, популярный детектор лиц на основе глубокого обучения, для обнаружения лиц на изображении. Если лицо обнаружено, функция вырезает лицо из изображения, используя координаты ограничивающей рамки, возвращенные MTCNN.

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

Затем функция извлекает сопоставление идентификаторов классов с метками классов (т. е. именами эмоций), используя конфигурацию предварительно обученной модели trpakov/vit-face-expression. Это сопоставление используется для преобразования вероятностей класса (преобразованных в список Python) в словарь, который связывает каждую метку эмоции с соответствующей вероятностью.

Наконец, функция использует matplotlib и seaborn для создания фигуры с двумя подграфиками — один для отображения обнаруженного лица, а другой для визуализации вероятностей эмоций в виде горизонтальной гистограммы. Полосы имеют цветовую кодировку в соответствии с эмоциями, которые они представляют. Полученный рисунок обеспечивает визуальное понимание прогнозов модели обнаружения эмоций для данного кадра изображения.

def detect_emotions(image):
    """
    Detect emotions from a given image, displays the detected
    face and the emotion probabilities in a bar plot.

    Parameters:
    image (PIL.Image): The input image.

    Returns:
    PIL.Image: The cropped face from the input image.
    """
    
    # Create a copy of the image to draw on
    temporary = image.copy()

    # Use the MTCNN model to detect faces in the image
    sample = mtcnn.detect(temporary)

    # If a face is detected
    if sample[0] is not None:
        
        # Get the bounding box coordinates of the face
        box = sample[0][0]
        
        # Crop the detected face from the image
        face = temporary.crop(box)

        # Pre-process the cropped face to be fed into the
        # emotion detection model
        inputs = extractor(images=face, return_tensors="pt")

        # Pass the pre-processed face through the model to
        # get emotion predictions
        outputs = model(**inputs)

        # Apply softmax to the logits to get probabilities
        probabilities = torch.nn.functional.softmax(outputs.logits,
                                                    dim=-1)

        # Retrieve the id2label attribute from the configuration
        id2label = AutoConfig.from_pretrained(
            "trpakov/vit-face-expression"
        ).id2label

        # Convert probabilities tensor to a Python list
        probabilities = probabilities.detach().numpy().tolist()[0]

        # Map class labels to their probabilities
        class_probabilities = {id2label[i]: prob for i,
                               prob in enumerate(probabilities)}

        # Define colors for each emotion
        colors = {
            "angry": "red",
            "disgust": "green",
            "fear": "gray",
            "happy": "yellow",
            "neutral": "purple",
            "sad": "blue",
            "surprise": "orange"
        }
        palette = [colors[label] for label in class_probabilities.keys()]

        # Prepare a figure with 2 subplots: one for the face image,
        # one for the bar plot
        fig, axs = plt.subplots(1, 2, figsize=(15, 6))

        # Display the cropped face in the left subplot
        axs[0].imshow(np.array(face))
        axs[0].axis('off')

        # Create a horizontal bar plot of the emotion probabilities in
        # the right subplot
        sns.barplot(ax=axs[1],
                    y=list(class_probabilities.keys()),
                    x=[prob * 100 for prob in class_probabilities.values()],
                    palette=palette,
                    orient='h')
        axs[1].set_xlabel('Probability (%)')
        axs[1].set_title('Emotion Probabilities')
        axs[1].set_xlim([0, 100])  # Set x-axis limits to show percentages

        # Show the plot
        plt.show()
# Initialize MTCNN model for single face cropping
mtcnn = MTCNN(
    image_size=160,
    margin=0,
    min_face_size=200,
    thresholds=[0.6, 0.7, 0.7],
    factor=0.709,
    post_process=True,
    keep_all=False,
    device=device
)
    
# Load the pre-trained model and feature extractor
extractor = AutoFeatureExtractor.from_pretrained(
    "trpakov/vit-face-expression"
)
model = AutoModelForImageClassification.from_pretrained(
    "trpakov/vit-face-expression"
)

Этот раздел кода используется для проверки нашей функции обнаружения эмоций на разных кадрах видео. Пять кадров (10-й, 40-й, 200-й, 355-й и 380-й) выбираются произвольно, чтобы обеспечить разнообразный образец видео. Для каждого выбранного кадра выполняются следующие шаги:

  1. Кадр извлекается из видеоданных (которые хранятся в виде списка массивов numpy, представляющих отдельные кадры).
  2. Кадр преобразуется в объект PIL Image. Это необходимо, потому что наша функция detect_emotions ожидает ввода в виде изображения PIL.
  3. Функция detect_emotions вызывается с изображением PIL в качестве входных данных. Эта функция обнаруживает лицо на изображении, прогнозирует вероятность различных эмоций и отображает обнаруженное лицо рядом с гистограммой прогнозируемых вероятностей эмоций.

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

# Choose a frame
frame = video_data[10]  # choosing the 10th frame

# Convert the frame to a PIL image and display it
image = Image.fromarray(frame)
detect_emotions(image)

# Choose a frame
frame = video_data[40]  # choosing the 40th frame

# Convert the frame to a PIL image and display it
image = Image.fromarray(frame)
detect_emotions(image)

# Choose a frame
frame = video_data[200]  # choosing the 200th frame

# Convert the frame to a PIL image and display it
image = Image.fromarray(frame)
detect_emotions(image)

# Choose a frame
frame = video_data[355]  # choosing the 355th frame

# Convert the frame to a PIL image and display it
image = Image.fromarray(frame)
detect_emotions(image)

# Choose a frame
frame = video_data[380]  # choosing the 380th frame

# Convert the frame to a PIL image and display it
image = Image.fromarray(frame)
detect_emotions(image)

В запоминающейся автомобильной сцене из фильма «Белые цыпочки» мы видим персонажей Латрелла Спенсера и Маркуса Коупленда, который работает под прикрытием как Тиффани Уилсон, которые вместе едут на машине. Во время их живого пения под песню Ванессы Карлтон «Тысяча миль» наша система распознавания эмоций выделяет интересную динамику.

Маркус Коупленд (Марлон Уэйанс), замаскированный под Тиффани Уилсон, проявляет эмоции страха и печали, возможно, отражение дискомфорта и беспокойства персонажа в этом веселом сценарии. С другой стороны, Латрелл Спенсер (Терри Крюс), который с энтузиазмом распевает песню, по мнению нашей модели, демонстрирует счастье.

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

Покадровое обнаружение эмоций

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

Первый шаг включает в себя обнаружение лиц на изображении с использованием модели многозадачных каскадных сверточных сетей (MTCNN), как и в нашем обнаружении одного кадра. Если на изображении идентифицируется лицо, его область извлекается для формирования нового обрезанного изображения.

На втором этапе это обрезанное изображение лица подвергается предварительной обработке для подготовки к модели обнаружения эмоций. Эта процедура включает преобразование изображения в тензор PyTorch, который модель может эффективно интерпретировать. Затем тензор проходит через нашу модель обнаружения эмоций, основанную на архитектуре Vision Transformer (ViT) — современной модели для задач классификации изображений.

Модель генерирует прогнозы в виде логитов для каждой эмоции. Затем функция softmax преобразует эти логиты в вероятности. Используя атрибут id2label из конфигурации модели, мы сопоставляем вероятности с соответствующими метками эмоций. В результате получается словарь, содержащий предсказанные эмоции и связанные с ними вероятности.

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

def detect_emotions(image):
    """
    Detect emotions from a given image.
    Returns a tuple of the cropped face image and a
    dictionary of class probabilities.
    """
    temporary = image.copy()

    # Detect faces in the image using the MTCNN group model
    sample = mtcnn.detect(temporary)
    if sample[0] is not None:
        box = sample[0][0]

        # Crop the face
        face = temporary.crop(box)

        # Pre-process the face
        inputs = extractor(images=face, return_tensors="pt")

        # Run the image through the model
        outputs = model(**inputs)

        # Apply softmax to the logits to get probabilities
        probabilities = torch.nn.functional.softmax(outputs.logits,
                                                    dim=-1)

        # Retrieve the id2label attribute from the configuration
        config = AutoConfig.from_pretrained(
            "trpakov/vit-face-expression"
        )
        id2label = config.id2label

        # Convert probabilities tensor to a Python list
        probabilities = probabilities.detach().numpy().tolist()[0]

        # Map class labels to their probabilities
        class_probabilities = {
            id2label[i]: prob for i, prob in enumerate(probabilities)
        }

        return face, class_probabilities
    return None, None

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

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

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

Затем функция преобразует фигуру matplotlib, содержащую лицо и график, в массив numpy, используя буфер холста. Это преобразование необходимо для реконструкции видео и для сохранения целостности сгенерированного визуального контента. Наконец, функция закрывает фигуру, чтобы освободить память, и возвращает массив numpy, представляющий объединенное изображение. Это комбинированное изображение обеспечивает согласованную визуальную сводку, эффективно инкапсулируя изображение лица и соответствующие ему эмоциональные вероятности.

def create_combined_image(face, class_probabilities):
    """
    Create an image combining the detected face and a barplot
    of the emotion probabilities.

    Parameters:
    face (PIL.Image): The detected face.
    class_probabilities (dict): The probabilities of each
        emotion class.

    Returns:
    np.array: The combined image as a numpy array.
    """
    # Define colors for each emotion
    colors = {
        "angry": "red",
        "disgust": "green",
        "fear": "gray",
        "happy": "yellow",
        "neutral": "purple",
        "sad": "blue",
        "surprise": "orange"
    }
    palette = [colors[label] for label in class_probabilities.keys()]
    
    # Create a figure with 2 subplots: one for the
    # face image, one for the barplot
    fig, axs = plt.subplots(1, 2, figsize=(15, 6))

    # Display face on the left subplot
    axs[0].imshow(np.array(face))
    axs[0].axis('off')

    # Create a barplot of the emotion probabilities
    # on the right subplot
    sns.barplot(ax=axs[1],
                y=list(class_probabilities.keys()),
                x=[prob * 100 for prob in class_probabilities.values()],
                palette=palette,
                orient='h')
    axs[1].set_xlabel('Probability (%)')
    axs[1].set_title('Emotion Probabilities')
    axs[1].set_xlim([0, 100])  # Set x-axis limits

    # Convert the figure to a numpy array
    canvas = FigureCanvas(fig)
    canvas.draw()
    img = np.frombuffer(canvas.tostring_rgb(), dtype='uint8')
    img  = img.reshape(canvas.get_width_height()[::-1] + (3,))
    
    plt.close(fig)
    return img

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

skips = 2
reduced_video = []

for i in tqdm(range(0, len(video_data), skips)):
    reduced_video.append(video_data[i])

Этот раздел кода управляет основными задачами проекта. Для каждого кадра в сокращенном наборе видеоданных скрипт выполняет следующие действия:

  1. Сначала он преобразует фрейм в соответствующий тип данных (uint8), чтобы он был совместим с функцией detect_emotions.
  2. Затем вызывается функция detect_emotions, принимающая в качестве входных данных каждый кадр, преобразованный в изображение PIL. Функция возвращает обрезанное лицо (если оно найдено) и связанные с ним вероятности эмоций.
  3. Если в кадре было обнаружено лицо, создается составное изображение, объединяющее обрезанное лицо и столбчатую диаграмму, отображающую вероятности эмоций. Затем это изображение добавляется в список combined_images для дальнейшего использования.
  4. Если в кадре не было обнаружено ни одного лица, вероятность эмоций устанавливается на None для этого конкретного кадра.
  5. Наконец, вероятности эмоций (или None, если лицо не было обнаружено) добавляются к списку all_class_probabilities. Этот список накапливает вероятности эмоций для каждого кадра и составляет ключевую часть выходных данных для проекта.
# Define a list of emotions
emotions = ["angry", "disgust", "fear", "happy", "neutral", "sad", "surprise"]

# List to hold the combined images
combined_images = []

# Create a list to hold the class probabilities for all frames
all_class_probabilities = []

# Loop over video frames
for i, frame in tqdm(enumerate(reduced_video),
                     total=len(reduced_video),
                     desc="Processing frames"):
    # Convert frame to uint8
    frame = frame.astype(np.uint8)

    # Call detect_emotions to get face and class probabilities
    face, class_probabilities = detect_emotions(Image.fromarray(frame))
    
    # If a face was found
    if face is not None:
        # Create combined image for this frame
        combined_image = create_combined_image(face, class_probabilities)
        
        # Append combined image to the list
        combined_images.append(combined_image)
    else:
        # If no face was found, set class probabilities to None
        class_probabilities = {emotion: None for emotion in emotions}
        
    # Append class probabilities to the list
    all_class_probabilities.append(class_probabilities)

В этом сегменте сценария список объединенных изображений, сгенерированный на предыдущем шаге, преобразуется обратно в видеоклип с помощью функции ImageSequenceClip из библиотеки moviepy.editor. Параметр fps (кадры в секунду) задается в соответствии с частотой кадров исходного видео, разделенной на количество пропусков кадров, сделанных ранее.

Затем видео отображается в среде IPython с использованием метода ipython_display. Это видео визуализирует результаты обнаружения эмоций на покадровой основе, при этом каждый кадр отображает обнаруженное лицо и гистограмму, показывающую распределение вероятности каждой эмоции. Результат может предоставить полезную информацию об эмоциональной динамике на протяжении всего видео.

# Convert list of images to video clip
clip_with_plot = ImageSequenceClip(combined_images,
                                   fps=vid_fps/skips)  # Choose the frame rate (fps) according to your requirement

# Write the video to a file with a specific frame rate
clip_with_plot.write_videofile("output_video.mp4", fps=vid_fps/skips)

# Display the clip
clip_with_plot.ipython_display(width=900)

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

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

# Define colors for each emotion
colors = {
    "angry": "red",
    "disgust": "green",
    "fear": "gray",
    "happy": "yellow",
    "neutral": "purple",
    "sad": "blue",
    "surprise": "orange"
}

# Convert list of class probabilities into a DataFrame
df = pd.DataFrame(all_class_probabilities)

# Convert probabilities to percentages
df = df * 100

# Create a line plot
plt.figure(figsize=(15,8))
for emotion in df.columns:
    plt.plot(df[emotion], label=emotion, color=colors[emotion])

plt.xlabel('Frame Order')
plt.ylabel('Emotion Probability (%)')
plt.title('Emotion Probabilities Over Time')
plt.legend()
plt.show()

Заключение

Этот проект успешно продемонстрировал, как применять глубокое обучение к задаче обнаружения эмоций в видео с использованием модели на основе Transformer, в частности ViT. Мы достигли этого, проанализировав кадры из выбранной сцены в фильме «Белые цыпочки», выявив эмоции, которые демонстрировали персонажи Латрелл Спенсер и Маркус/Тиффани во время комично запоминающейся поездки на автомобиле.

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

Рекомендации

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

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

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

Документация по генеративному ИИ

Приложение OpenAI GPT-4, или ChatGPT, было ключевым компонентом в выполнении этого проекта. Его интеграция охватила несколько этапов проекта:

  1. Идея. Модель ИИ использовалась в качестве партнера по мозговому штурму, определяя цели проекта и шаги для их достижения.
  2. Помощь в написании кода. ChatGPT был надежным помощником для решения задач кодирования, включая понимание ошибок, отладку и предоставление решений для сложных задач кодирования.
  3. Документация и комментирование. Модель ИИ руководила процессом комментирования кода и документирования процедур, объясняя сложные задачи простым и доступным языком.
  4. Рассказывание историй. ChatGPT был использован для создания убедительного повествования о целях, выводах и последствиях проекта, помогая контекстуализировать техническую работу в более широком контексте.
  5. Написание отчета и корректура. ChatGPT сыграл важную роль в написании этого отчета, обеспечив ясность, точность и удобочитаемость, а также являясь профессиональным корректором.

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

Рекомендации

  1. https://towardsdatascience.com/training-an-emotion-detector-with-transfer-learning-91dea84adeed
  2. https://huggingface.co/trpakov/vit-face-expression