31、面部表情识别:从数据收集到模型训练

面部表情识别:从数据收集到模型训练

1. 面部区域处理与表情识别流程概述

在面部表情识别中,首先需要对采集到的面部图像进行处理,以确保数据的一致性和可用性。我们会计算一个缩放因子,将两眼之间的距离缩放至图像宽度的 50%,计算公式如下:

eye_size_scale = (1.0 - desired_eye_x * 2) * 
    desired_img_width / np.linalg.norm(
    right_eye_center – left_eye_center)

利用计算得到的两个参数( eye_angle_deg eye_size_scale ),可以生成一个合适的旋转矩阵来变换图像:

rot_mat = cv2.getRotationMatrix2D(tuple(eye_center), 
eye_angle_deg, eye_size_scale)

为了确保眼睛中心处于图像中心,还需要对旋转矩阵进行调整:

rot_mat[0,2] += desired_img_width*0.5 – eye_center[0]
rot_mat[1,2] += desired_eye_y*desired_img_height – 
    eye_center[1]

最后,通过 cv2.warpAffine 函数得到面部区域的直立缩放版本:

res = cv2.warpAffine(head, rot_mat, 
    (desired_img_width, desired_img_height))

面部表情识别流程主要包含以下三个步骤:
1. 以训练模式加载 GUI 应用,收集训练集数据。
2. 通过 train_test_mlp.py 脚本在训练集上训练 MLP 分类器,并将训练好的权重保存到文件中。
3. 以测试模式加载 GUI 应用,对实时视频流中的面部表情进行分类。

2. 训练集的收集

在训练 MLP 之前,需要收集合适的训练集。由于个人面部数据可能不在现有的数据集中,因此需要自己收集。可以使用之前章节的 GUI 应用,该应用可以访问网络摄像头,并对视频流的每一帧进行操作。

2.1 运行屏幕捕获应用

要运行 chapter7.py 应用,需要使用 cv2.VideoCapture 设置屏幕捕获,并将句柄传递给 FaceLayout 类,代码如下:

import time
import wx
from os import path
import cPickle as pickle
import cv2
import numpy as np
from datasets import homebrew
from detectors import FaceDetector
from classifiers import MultiLayerPerceptron
from gui import BaseLayout

def main():
    capture = cv2.VideoCapture(0)
    if not(capture.isOpened()):
        capture.open()
    capture.set(cv2.cv.CV_CAP_PROP_FRAME_WIDTH, 640)
    capture.set(cv2.cv.CV_CAP_PROP_FRAME_HEIGHT, 480)
    # start graphical user interface
    app = wx.App()
    layout = FaceLayout(None, -1, 'Facial Expression Recognition', 
        capture)
    layout.init_algorithm()
    layout.Show(True)
    app.MainLoop()

if __name__ == '__main__':
    main()

如果安装的是一些非标准版本的 OpenCV,帧宽度和高度参数的名称可能会有所不同。在较新的版本中,访问旧的 OpenCV1 子模块 cv 及其变量 cv2.cv.CV_CAP_PROP_FRAME_WIDTH cv2.cv.CV_CAP_PROP_FRAME_HEIGHT 是最简单的方法。

2.2 GUI 构造与初始化

应用的 GUI 是 BaseLayout 的定制版本,在初始化时,会将训练样本和标签初始化为空列表,并确保在关闭窗口时调用 _on_exit 方法,将训练数据保存到文件中:

class FaceLayout(BaseLayout):
    def _init_custom_layout(self):
        # initialize data structure
        self.samples = []
        self.labels = []
        # call method to save data upon exiting
        self.Bind(wx.EVT_CLOSE, self._on_exit)

    def init_algorithm(
        self, 
        save_training_file='datasets/faces_training.pkl',
        load_preprocessed_data='datasets/faces_preprocessed.pkl',
        load_mlp='params/mlp.xml',
        face_casc='params/haarcascade_frontalface_default.xml',
        left_eye_casc='params/haarcascade_lefteye_2splits.xml',
        right_eye_casc='params/haarcascade_righteye_2splits.xml'):
        self.dataFile = save_training_file
        self.faces = FaceDetector(face_casc, left_eye_casc, 
            right_eye_casc)
        if path.isfile(load_preprocessed_data):
            (_, y_train), (_, y_test), self.pca_V, self.pca_m =
                homebrew.load_from_file(load_preprocessed_data)
            self.all_labels = np.unique(np.hstack((y_train, 
                y_test)))
            if path.isfile(load_mlp):
                self.MLP = MultiLayerPerceptron( 
                    np.array([self.pca_V.shape[1], 
                    len(self.all_labels)]), 
                    self.all_labels)
                self.MLP.load(load_mlp)
            else:
                print "Warning: Testing is disabled"
                print "Could not find pre-trained MLP file ", 
                    load_mlp
                self.testing.Disable()
        else:
            print "Warning: Testing is disabled"
            print "Could not find preprocessed data file ", 
                loadPreprocessedData
        self.testing.Disable()

2.3 GUI 布局设计

GUI 布局的创建由 _create_custom_layout 方法完成,布局尽量简单,包含一个用于显示视频帧的面板和下面的一排按钮。用户可以点击六个单选按钮之一来选择要记录的面部表情,然后将头部置于边界框内,点击“Take Snapshot”按钮进行拍照。在当前相机帧下方,有两个单选按钮用于选择训练或测试模式,代码如下:

def _create_custom_layout(self):
    # create horizontal layout with train/test buttons
    pnl1 = wx.Panel(self, -1)
    self.training = wx.RadioButton(pnl1, -1, 'Train', (10, 10), 
        style=wx.RB_GROUP)
    self.testing = wx.RadioButton(pnl1, -1, 'Test')
    hbox1 = wx.BoxSizer(wx.HORIZONTAL)
    hbox1.Add(self.training, 1)
    hbox1.Add(self.testing, 1)
    pnl1.SetSizer(hbox1)
    self.Bind(wx.EVT_RADIOBUTTON, self._on_training, 
        self.training)
    self.Bind(wx.EVT_RADIOBUTTON, self._on_testing, self.testing)

    # create a horizontal layout with all buttons
    pnl2 = wx.Panel(self, -1 )
    self.neutral = wx.RadioButton(pnl2, -1, 'neutral', 
        (10, 10), style=wx.RB_GROUP)
    self.happy = wx.RadioButton(pnl2, -1, 'happy')
    self.sad = wx.RadioButton(pnl2, -1, 'sad')
    self.surprised = wx.RadioButton(pnl2, -1, 'surprised')
    self.angry = wx.RadioButton(pnl2, -1, 'angry')
    self.disgusted = wx.RadioButton(pnl2, -1, 'disgusted')
    hbox2 = wx.BoxSizer(wx.HORIZONTAL)
    hbox2.Add(self.neutral, 1)
    hbox2.Add(self.happy, 1)
    hbox2.Add(self.sad, 1)
    hbox2.Add(self.surprised, 1)
    hbox2.Add(self.angry, 1)
    hbox2.Add(self.disgusted, 1)
    pnl2.SetSizer(hbox2)

    pnl3 = wx.Panel(self, -1)
    self.snapshot = wx.Button(pnl3, -1, 'Take Snapshot')
    self.Bind(wx.EVT_BUTTON, self.OnSnapshot, self.snapshot)
    hbox3 = wx.BoxSizer(wx.HORIZONTAL)
    hbox3.Add(self.snapshot, 1)
    pnl3.SetSizer(hbox3)

    # display the button layout beneath the video stream
    self.panels_vertical.Add (pnl1, flag=wx.EXPAND | wx.TOP, border=1)
    self.panels_vertical.Add(pnl2, flag=wx.EXPAND | wx.BOTTOM, 
        border=1)
    self.panels_vertical.Add(pnl3, flag=wx.EXPAND | wx.BOTTOM, 
        border=1)

当用户选择测试模式时,会调用 _on_testing 方法禁用所有与训练相关的按钮;选择训练模式时,会调用 _on_training 方法启用这些按钮:

def _on_testing(self, evt):
    """Whenever testing mode is selected, disable all 
        training-related buttons"""
    self.neutral.Disable()
    self.happy.Disable()
    self.sad.Disable()
    self.surprised.Disable()
    self.angry.Disable()
    self.disgusted.Disable()
    self.snapshot.Disable()

def _on_training(self, evt):
    """Whenever training mode is selected, enable all 
        training-related buttons"""
    self.neutral.Enable()
    self.happy.Enable()
    self.sad.Enable()
    self.surprised.Enable()
    self.angry.Enable()
    self.disgusted.Enable()
    self.snapshot.Enable()

2.4 处理当前帧

_process_frame 方法用于处理当前帧,首先在当前帧的降采样灰度版本中检测面部:

def _process_frame(self, frame):
    success, frame, self.head = self.faces.detect(frame)
    if success:
        # 可以访问当前帧的注释版本和提取的头部区域
        pass
    return frame

2.5 添加训练样本到训练集

当点击“Take Snapshot”按钮时,会调用 _on_snapshot 方法,该方法会根据单选按钮的值检测要记录的情绪表达,并分配相应的类标签。然后对检测到的面部区域进行对齐处理,将成功对齐的样本添加到数据集:

def _on_snapshot(self, evt):
    if self.neutral.GetValue():
        label = 'neutral'
    elif self.happy.GetValue():
        label = 'happy'
    elif self.sad.GetValue():
        label = 'sad'
    elif self.surprised.GetValue():
        label = 'surprised'
    elif self.angry.GetValue():
        label = 'angry'
    elif self.disgusted.GetValue():
        label = 'disgusted'
    if self.head is None:
        print "No face detected"
    else:
        success, head = self.faces.align_head(self.head)
        if success:
            print "Added sample to training set"
            self.samples.append(head.flatten())
            self.labels.append(label)
        else:
            print "Could not align head (eye detection failed?)"

2.6 保存训练集到文件

当退出应用时,会触发 EVT_CLOSE 事件,调用 _on_exit 方法将收集的样本和相应的类标签保存到文件中。为了避免意外覆盖现有训练集,如果提供的文件名已存在,会添加后缀并保存到新文件名:

def _on_exit(self, evt):
    """Called whenever window is closed"""
    if len(self.samples) > 0:
        if path.isfile(self.data_file):
            filename, fileext = path.splitext(self.data_file)
            offset = 0
            while True:
                file = filename + "-" + str(offset) + fileext
                if path.isfile(file):
                    offset += 1
                else:
                    break
            self.data_file = file
        f = open(self.dataFile, 'wb')
        pickle.dump(self.samples, f)
        pickle.dump(self.labels, f)
        f.close()
        print "Saved", len(self.samples), "samples to", self.data_file
        self.Destroy()

3. 特征提取与主成分分析(PCA)

在完成训练集的收集后,需要进行特征提取,以找到最能描述数据的特征。这里使用主成分分析(PCA)作为特征提取方法。

3.1 数据集预处理

dataset/homebrew.py 中定义了 load_data 函数,用于解析数据集、进行特征提取、将数据拆分为训练集和测试集,并返回结果:

import cv2
import numpy as np
import csv
from matplotlib import cm
from matplotlib import pyplot as plt
from os import path
import cPickle as pickle

def load_data(load_from_file, test_split=0.2, num_components=50, 
    save_to_file=None, plot_samples=False, seed=113):
    """load dataset from pickle """
    X = []
    labels = []
    if not path.isfile(load_from_file):
        print "Could not find file", load_from_file
        return (X, labels), (X, labels), None, None
    else:
        print "Loading data from", load_from_file
        f = open(load_from_file, 'rb')
        samples = pickle.load(f)
        labels = pickle.load(f)
        print "Loaded", len(samples), "training samples"
    X, V, m = extract_features(samples, 
        num_components=num_components)
    np.random.seed(seed)
    np.random.shuffle(X)
    np.random.seed(seed)
    np.random.shuffle(labels)
    X_train = X[:int(len(X)*(1-test_split))]
    y_train = labels[:int(len(X)*(1-test_split))]
    X_test = X[int(len(X)*(1-test_split)):]
    y_test = labels[int(len(X)*(1-test_split)):]
    if save_to_file is not None:
        f = open(save_to_file, 'wb')
        pickle.dump(X_train, f)
        pickle.dump(y_train, f)
        pickle.dump(X_test, f)
        pickle.dump(y_test, f)
        pickle.dump(V, f)
        pickle.dump(m, f)
        f.close()
        print "Save preprocessed data to", save_to_file
    return (X_train, y_train), (X_test, y_test), V, m

3.2 主成分分析(PCA)原理

PCA 是一种降维技术,适用于处理高维数据。可以将图像看作高维空间中的一个点,通过 PCA 可以找到一个更小、更紧凑的向量空间,用更少的特征来描述图像。在 OpenCV 中,使用 cv2.PCACompute 函数进行 PCA 计算,代码如下:

def extract_feature(X, V=None, m=None, num_components=50):
    if V is None or m is None:
        if num_components is None:
            num_components = 50
        Xarr = np.squeeze(np.array(X).astype(np.float32))
        m, V = cv2.PCACompute(Xarr)
    V = V[:num_components]
    for i in xrange(len(X)):
        X[i] = np.dot(V, X[i] - m[0, i])
    return X, V, m

4. 多层感知器(MLP)

多层感知器(MLP)是一种人工神经网络,用于将一组输入数据转换为一组输出数据。MLP 的核心是感知器,它类似于生物神经元。通过将多个感知器组合成多层网络,MLP 能够对输入数据做出非线性决策,并可以使用反向传播算法进行训练,适用于监督学习任务。

4.1 感知器

感知器是一个二进制分类器,由 Frank Rosenblatt 在 20 世纪 50 年代发明。它计算输入的加权和,如果和超过阈值,则输出 1,否则输出 0。感知器的输出由非线性激活函数决定,原始算法中使用的是 Heaviside 阶跃函数,现代实现中可以使用从 sigmoid 到双曲正切函数等多种激活函数。

4.2 深度架构

MLP 通常由至少三层组成,第一层为输入层,每个节点对应数据集中的一个输入特征;最后一层为输出层,每个节点对应一个类标签;中间层为隐藏层。在前馈神经网络中,输入层的部分或所有节点与隐藏层的所有节点相连,隐藏层的部分或所有节点与输出层的部分或所有节点相连。

增加神经网络的神经元数量通常会降低训练集的错误率,但测试集的错误率可能会先下降后上升。这是因为模型的复杂度增加会提高其表达能力,但也可能导致过拟合。因此,需要通过试验和调整网络大小来找到最佳的泛化性能。

4.3 MLP 用于面部表情识别

为了实现面部表情识别,我们开发了一个 MultiLayerPerceptron 类,继承自分类器基类 Classifier

from abc import ABCMeta, abstractmethod

class Classifier:
    """Abstract base class for all classifiers"""
    __metaclass__ = ABCMeta
    @abstractmethod
    def fit(self, X_train, y_train):
        pass
    @abstractmethod
    def evaluate(self, X_test, y_test, visualize=False):
        pass

class MultiLayerPerceptron(Classifier):
    def __init__(self, layer_sizes, class_labels, params=None):
        self.num_features = layer_sizes[0]
        self.num_classes = layer_sizes[-1]
        self.class_labels = class_labels
        self.params = params or dict()
        self.model = cv2.ANN_MLP()
        self.model.create(layer_sizes)

    def _labels_str_to_num(self, labels):
        """ convert string labels to their corresponding ints """
        return np.array([int(np.where(self.class_labels == l)[0]) 
            for l in labels])

    def _labels_num_to_str(self, labels):
        """Convert integer labels to their corresponding string 
            names """
        return self.class_labels[labels]

    def load(self, file):
        """ load a pre-trained MLP from file """
        self.model.load(file)

    def save(self, file):
        """ save a trained MLP to file """
        self.model.save(file)

    def fit(self, X_train, y_train, params=None):
        """ fit model to data """
        if params is None:
            params = self.params
        y_train = self._labels_str_to_num(y_train)
        y_train = self._one_hot(y_train).reshape(-1, 
            self.num_classes)
        self.model.train(X_train, y_train, None, params=params)

    def _one_hot(self, y_train):
        """Convert a list of labels into one-hot code """
        num_samples = len(y_train)
        new_responses = np.zeros(num_samples * self.num_classes, 
            np.float32)
        resp_idx = np.int32(y_train + 
            np.arange(num_samples) * self.num_classes)
        new_responses[resp_idx] = 1
        return new_responses

    def evaluate(self, X_test, y_test, visualize=False):
        """ evaluate model performance """
        ret, Y_vote = self.model.predict(X_test)
        y_test = self._labels_str_to_num(y_test)
        accuracy = self._accuracy(y_test, Y_vote)
        precision = self._precision(y_test, Y_vote)
        recall = self._recall(y_test, Y_vote)
        return (accuracy, precision, recall)

    def predict(self, X_test):
        """ predict the labels of test data """
        ret, Y_vote = self.model.predict(X_test)
        y_hat = np.argmax(Y_vote, 1)
        return self._labels_num_to_str(y_hat)

4.4 运行脚本训练和测试 MLP

使用 train_test_mlp.py 脚本可以训练和测试 MLP 分类器。脚本首先解析自制数据集并提取所有类标签,然后设置训练参数,通过循环尝试不同大小的 MLP 网络,找到最佳配置并保存到文件中:

import cv2
import numpy as np
from datasets import homebrew
from classifiers import MultiLayerPerceptron

def main():
    (X_train, y_train),(X_test, y_test) =
        homebrew.load_data("datasets/faces_training.pkl", 
        num_components=50, test_split=0.2, 
        save_to_file="datasets/faces_preprocessed.pkl", 
        seed=42)
    if len(X_train) == 0 or len(X_test) == 0:
        print "Empty data"
        raise SystemExit
    X_train = np.squeeze(np.array(X_train)).astype(np.float32)
    y_train = np.array(y_train)
    X_test = np.squeeze(np.array(X_test)).astype(np.float32)
    y_test = np.array(y_test)
    labels = np.unique(np.hstack((y_train, y_test)))
    params = dict( term_crit = (cv2.TERM_CRITERIA_COUNT, 300, 
        0.01), train_method=cv2.ANN_MLP_TRAIN_PARAMS_BACKPROP, 
        bp_dw_scale=0.001, bp_moment_scale=0.9 )
    save_file = 'params/mlp.xml'
    num_features = len(X_train[0])
    num_classes = len(labels)
    print "1-hidden layer networks"
    best_acc = 0.0
    for l1 in xrange(10):
        layer_sizes = np.int32([num_features, 
            (l1 + 1) * num_features / 5, 
            num_classes])
        MLP = MultiLayerPerceptron(layer_sizes, labels)
        print layer_sizes

综上所述,面部表情识别是一个复杂的过程,涉及数据收集、特征提取和模型训练等多个步骤。通过合理运用 PCA 和 MLP 等技术,可以实现对实时视频流中面部表情的准确分类。在实际应用中,还需要根据具体任务对模型进行调优,以达到最佳的泛化性能。

下面是面部表情识别的主要流程 mermaid 流程图:

graph LR
    A[收集训练集] --> B[特征提取]
    B --> C[训练 MLP 分类器]
    C --> D[保存训练好的模型]
    D --> E[实时视频流测试]

面部表情识别步骤总结表格:
| 步骤 | 描述 |
| ---- | ---- |
| 收集训练集 | 使用 GUI 应用收集不同表情的面部图像,保存为训练集文件 |
| 特征提取 | 使用 PCA 对训练集进行降维处理,提取主要特征 |
| 训练 MLP 分类器 | 在训练集上训练 MLP 分类器,调整参数以优化性能 |
| 保存训练好的模型 | 将训练好的 MLP 模型保存到文件中 |
| 实时视频流测试 | 加载模型,对实时视频流中的面部表情进行分类 |

5. 面部表情识别的实际应用与优化建议

5.1 实际应用场景

面部表情识别技术在多个领域有着广泛的应用:
- 人机交互 :在智能设备中,通过识别用户的面部表情,设备可以根据用户的情绪状态提供更个性化的服务。例如,当识别到用户开心时,播放欢快的音乐;当识别到用户疲惫时,提醒用户休息。
- 市场调研 :在商场、超市等场所,通过分析顾客的面部表情,可以了解顾客对商品的喜好程度和满意度,为商家的营销策略提供参考。
- 教育领域 :教师可以通过识别学生的面部表情,了解学生的学习状态和兴趣点,及时调整教学方法和内容,提高教学效果。

5.2 优化建议

为了提高面部表情识别的准确性和性能,可以从以下几个方面进行优化:
- 数据增强 :在收集训练集时,可以通过旋转、翻转、缩放等方式对图像进行数据增强,增加训练数据的多样性,提高模型的泛化能力。
- 模型调优 :通过调整 MLP 模型的参数,如学习率、隐藏层神经元数量等,找到最佳的模型配置。也可以尝试使用其他深度学习模型,如卷积神经网络(CNN),来提高识别准确率。
- 多模态融合 :结合其他模态的数据,如语音、姿态等,进行多模态的表情识别,提高识别的准确性和可靠性。

6. 常见问题及解决方案

6.1 面部检测失败

  • 问题描述 :在处理视频帧时,可能会出现面部检测失败的情况,导致无法获取面部区域。
  • 解决方案 :检查使用的级联分类器文件是否正确,调整级联分类器的参数,如最小检测尺寸、检测阈值等。也可以尝试使用其他更先进的面部检测算法。

6.2 特征提取效果不佳

  • 问题描述 :PCA 特征提取可能无法准确地提取面部表情的特征,导致模型的识别准确率不高。
  • 解决方案 :尝试调整 PCA 的参数,如主成分数量。也可以使用其他特征提取方法,如局部二值模式(LBP)、梯度直方图(HOG)等,与 PCA 结合使用。

6.3 模型过拟合或欠拟合

  • 问题描述 :在训练 MLP 模型时,可能会出现过拟合或欠拟合的情况,导致模型在测试集上的性能不佳。
  • 解决方案 :对于过拟合问题,可以增加训练数据的数量,使用正则化方法,如 L1 和 L2 正则化,减少模型的复杂度。对于欠拟合问题,可以增加模型的复杂度,如增加隐藏层神经元数量。

7. 总结与展望

7.1 总结

面部表情识别是一个具有挑战性但又非常有价值的研究领域。通过本文介绍的方法,我们可以实现一个基本的面部表情识别系统,包括数据收集、特征提取、模型训练和实时测试。在实际应用中,需要根据具体任务对模型进行调优,以达到最佳的泛化性能。

7.2 展望

随着深度学习技术的不断发展,面部表情识别的准确性和性能将会不断提高。未来,我们可以期待更加智能、高效的面部表情识别系统的出现,为人们的生活和工作带来更多的便利。同时,面部表情识别技术也将与其他技术,如虚拟现实、增强现实等,进行深度融合,创造出更加丰富的应用场景。

下面是面部表情识别优化流程 mermaid 流程图:

graph LR
    A[数据收集] --> B[数据增强]
    B --> C[特征提取]
    C --> D[模型选择]
    D --> E[模型调优]
    E --> F[多模态融合]
    F --> G[实时测试]
    G --> H{性能评估}
    H -- 达标 --> I[应用部署]
    H -- 不达标 --> B

面部表情识别优化步骤总结表格:
| 步骤 | 描述 |
| ---- | ---- |
| 数据收集 | 收集不同表情的面部图像,构建训练集 |
| 数据增强 | 通过旋转、翻转等方式增加训练数据的多样性 |
| 特征提取 | 使用 PCA 等方法提取面部表情的特征 |
| 模型选择 | 选择合适的模型,如 MLP、CNN 等 |
| 模型调优 | 调整模型的参数,提高模型的性能 |
| 多模态融合 | 结合其他模态的数据,提高识别的准确性 |
| 实时测试 | 在实时视频流中测试模型的性能 |
| 性能评估 | 评估模型的准确性和泛化能力 |
| 应用部署 | 将优化后的模型部署到实际应用中 |

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值