前言

因为毕设使用到了OpenFace,趁着学习和使用的机会记录下入门的心得。

简介

OpenFace —— 一种旨在用于计算机视觉和机器学习研究人员,情感计算社区以及对基于面部行为分析构建交互式应用程序感兴趣的人们的工具。OpenFace能够进行脸部特征点检测,头部姿势估计,脸部动作单位识别和注视估计,并具有用于运行和训练模型的可用源代码。代表OpenFace核心的计算机视觉算法在上述所有任务中均展示了最新的技术成果。此外,我们的工具具有实时性能,并且可以通过简单的网络摄像头运行,而无需任何专业硬件。

OpenFace是一个在GitHub上开源的基于C++的,能够从图像或视频中识别出人脸,并标记出脸部特征点,检测出AU强度、注视方向以及脸部朝向等功能的软件。(另外还有一个更有名的openface,是基于python,用于人脸识别的,跟这个OpenFace没有关系)
项目地址:https://github.com/TadasBaltrusaitis/OpenFace

使用

项目同时提供了可直接使用的可执行文件和源码,这里分两部分讲述。

直接使用

https://github.com/TadasBaltrusaitis/OpenFace/releases处直接下载编译好的文件,解压。但是还不能运行,要额外下载模型(通过OneDrive):

四个模型文件全部下载完后,放入OpenFace文件夹下 model\patch_experts内。
文件夹下有数个可执行文件,这里挑三个常用的。

FaceLandmarkImg.exe

这个程序用来处理照片。在当前目录下打开控制台(CMD或powershell),运行

.\FaceLandmarkImg.exe -f .\samples\sample1.jpg

其中 -f 参数代表处理单个文件,后接文件路径,-fdir 参数则是处理文件夹。

等程序运行完毕后,会在目录下生成processed文件夹,里面包含有处理完(加上特征点与注视方向等内容)的照片和保存了处理数据(特征点坐标、AU强度等)的csv文件等。

原图

处理后

FeatureExtraction.exe

这个程序用来处理视频。在当前目录下打开控制台,运行

.\FeatureExtraction.exe -f .\samples\default.wmv

同样会在processed文件夹下生成对应的处理完的视频和csv文件等。

OpenFaceOffline.exe

这个程序则是带有UI界面的完全体,可以处理图片、视频甚至摄像头的实时画面。

UI界面

可以看到左边有处理后的视频画面,右边则包含了面部角度、注视角度和各个AU的强度
同样在结束后会在processed文件夹下生成之前提到的文件。

调用API

通过https://github.com/TadasBaltrusaitis/OpenFace/archive/master.zip下载源码,解压。
同样要下载前面提到的4个模型文件,放入 lib\local\LandmarkDetector\model\patch_experts中。

官方提供了API调用的文档:https://github.com/TadasBaltrusaitis/OpenFace/wiki/API-calls
比如检测照片中的特征点:

LandmarkDetector::FaceModelParameters det_parameters;
LandmarkDetector::CLNF face_model(det_parameters.model_location);

LandmarkDetector::DetectLandmarksInImage(rgb_image, face_model, det_parameters, grayscale_image);

前两行实例化照抄即可,用于加载模型,放在函数外,启动时便会自行加载。DetectLandmarksInImage中的参数:

  • rgb_imagecv::Mat类型的rgb图像。
  • grayscale_imagecv::Mat_<uchar>类型的灰度图像,可以声明一个空的,函数检测其为空时会自动生成。
  • 特征点信息保存在 face_model\detected\landmarks中,格式是 [x1;x2;...xn;y1;y2...yn]

我这里以C#和C++联合开发简单举例,在C#主程序中调用C++ dll,检测视频中人脸的特征点。

C++部分

右键LandmarkDetector项目,属性,选择生成为.dll。
在项目中添加新的C++文件,加入必要的头文件。
实例化需要的对象:

cv::Mat rgb_image;
cv::Mat_<uchar> grayscale_image;
vector<vector<float>> landmarks;
// 加载模型
LandmarkDetector::FaceModelParameters det_parameters;
LandmarkDetector::CLNF face_model(det_parameters.model_location);

然后编写一个供C#调用的函数

extern "C" __declspec(dllexport)int OpenVideo(char* file)
{
    VideoCapture cap;
    cap.open(file);

    // 获取总帧数 
    int frameCount = cap.get(CAP_PROP_FRAME_COUNT);

    // 设置特征点二维数组
    // 总共有68个特征点,前68为x坐标,后68为y坐标
    landmarks.resize(frameCount);
    for (int i = 0; i < landmarks.size(); i++)
        landmarks[i].resize(136);

    // 读取视频
    int frameNum = 0;
    while (cap.read(rgb_image))
    {
        cv::Mat_<uchar> grayscale_image;
        // 调用OpenFace提供的DetectLandmarksInVideo函数,和上面检测照片的基本相同
        bool detection_success = LandmarkDetector::DetectLandmarksInVideo(rgb_image, face_model, det_parameters, grayscale_image);
        if (detection_success)
        {
            // 记录特征点
            for (int i = 0; i < 136; i++)
            {
                landmarks[frameNum][i] = face_model.detected_landmarks.at<float>(0, i);
            }
        }
    }

    int result;
    /*
    ************************************************
    这里编写对landmarks的处理,处理完后得到结果,return回C#部分
    ************************************************
    */
    return result;
}

C#部分

在相同解决方案中添加一个C# winform项目,首先先导入dll,声明刚刚编写的外部函数。

[DllImport("LandmarkDetector.dll" , EntryPoint = "OpenVideo")]
public static extern int OpenVideo(byte[] path);

然后编写一个按钮点击函数,打开文件传入C++处理,接受返回。

private void openVideoToolStripMenuItem_Click(object sender, EventArgs e)
{
    OpenFileDialog dialog = new OpenFileDialog();
    dialog.Title = "请选择文件";
    dialog.Filter = "所有文件(*.*)|*.*";
    if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
    {
        // 不能直接传入字符串,转换成Byte[]
        byte[] file = System.Text.Encoding.Default.GetBytes(dialog.FileName);

        // 调用函数,得到结果
        int result = OpenVideo(file);
    }
}

然后编译运行测试。如果无法运行则要:

  • 将上述四个模型文件复制到编译目录的 model\patch_experts
  • 将opencv的dll文件复制编译目录(opencv版本视OpenFace版本决定)

而且注意Release版本的运行速度要远远高于Debug版本。

后记

本文只介绍了OpenFace最简单的使用方法,如要调用API获取AU等数据还请参考上面的官方文档。
同时上述代码只是大概介绍其使用方法,不一定能够保证正常编译和运行,如有错误,感谢指出。 如果随着毕设进行发现新的问题,应该会更新本文。

更新

更新一个通过AU检测表情的C++ dll,测试在Linux下编译通过。

#include <opencv2/core/core.hpp>
#include <LandmarkCoreIncludes.h>
#include <Face_utils.h>
#include <FaceAnalyser.h>

LandmarkDetector::FaceModelParameters det_parameters;
FaceAnalysis::FaceAnalyserParameters face_analysis_params;
LandmarkDetector::CLNF *face_model;
FaceAnalysis::FaceAnalyser *face_analyser;

// 初始化
extern "C" int Init()
{
    try
    {
        face_analysis_params.OptimizeForImages();
        face_model = new LandmarkDetector::CLNF(det_parameters.model_location);
        face_analyser = new FaceAnalysis::FaceAnalyser(face_analysis_params);
        face_model->face_detector_MTCNN.Read(det_parameters.mtcnn_face_detector_location);
        face_model->mtcnn_face_detector_location = det_parameters.mtcnn_face_detector_location;        return 1;
    }
    catch(...)
    {
        return 0;
    }
    
}

// 检测
extern "C" int GetMicroExpression(int height, int width, uchar* frame_data)
{
    cv::Mat captured_image(height, width, CV_8UC3, frame_data);
    cv::Mat grayscale_image;
    bool detection_success = LandmarkDetector::DetectLandmarksInVideo(captured_image, *face_model, det_parameters, grayscale_image);
    if(detection_success)
    {
        face_analyser->PredictStaticAUsAndComputeFeatures(captured_image, face_model->detected_landmarks);
        std::vector<std::pair<std::string, double>> AUsClass = face_analyser->GetCurrentAUsClass();
        std::map<std::string, double> mapAUsClass;
        for (size_t i = 0; i < AUsClass.size(); i++)
        {
            mapAUsClass.insert(AUsClass[i]);
        }
        if (mapAUsClass["AU01"] + mapAUsClass["AU02"] + mapAUsClass["AU04"] + mapAUsClass["AU05"] + mapAUsClass["AU07"] + mapAUsClass["AU20"] + mapAUsClass["AU25"] > 5.9)
            return 0;       // 恐惧
        else if (mapAUsClass["AU01"] + mapAUsClass["AU02"] + mapAUsClass["AU05"] + mapAUsClass["AU25"] + mapAUsClass["AU26"] > 3.9)
            return 1;       // 惊讶
        else if (mapAUsClass["AU01"] + mapAUsClass["AU04"] + mapAUsClass["AU07"] + mapAUsClass["AU15"] + mapAUsClass["AU17"] > 3.9)
            return 2;       // 悲伤
        else if (mapAUsClass["AU04"] + mapAUsClass["AU05"] + mapAUsClass["AU07"] + mapAUsClass["AU15"] > 2.9)
            return 3;       // 愤怒
        else if (mapAUsClass["AU09"] + mapAUsClass["AU10"] + mapAUsClass["AU17"] > 2.9)
            return 4;       // 厌恶
        else if (mapAUsClass["AU06"] + mapAUsClass["AU12"] > 1.9)
            return 5;       // 高兴
        else
            return -1;      // 无表情
    }
    else return -1;
}
如果觉得我的文章对你有用,请随意赞赏