目标
在本教程中,您将学习如何
- 创建 FacemarkAAM 的实例
- 训练 AAM 模型
- 使用 FacemarkAAM 进行拟合
准备
在继续本教程之前,您应该下载面部特征点检测的数据集。我们建议您下载 LFPW 数据集,可以从 https://ibug.doc.ic.ac.uk/download/annotations/lfpw.zip 获取。
确保注释格式受 API 支持,注释文件中的内容应如下所示:
version: 1
n_points: 68
{
212.716603 499.771793
230.232816 566.290071
...
}
接下来要做的是创建 2 个文本文件,分别包含图像文件列表和注释文件列表。确保两个文件中图像和注释的顺序匹配。此外,建议使用绝对路径而不是相对路径。在 Linux 机器中创建文件列表的示例:
ls $PWD/trainset/*.jpg > images_train.txt
ls $PWD/trainset/*.pts > annotation_train.txt
images_train.txt 文件内容示例
/home/user/lfpw/trainset/100032540_1.jpg
/home/user/lfpw/trainset/100040721_1.jpg
/home/user/lfpw/trainset/100040721_2.jpg
/home/user/lfpw/trainset/1002681492_1.jpg
annotation_train.txt 中的内容示例:
/home/user/lfpw/trainset/100032540_1.pts
/home/user/lfpw/trainset/100040721_1.pts
/home/user/lfpw/trainset/100040721_2.pts
/home/user/lfpw/trainset/1002681492_1.pts
可选地,您可以为测试集创建类似的文件。
在本教程中,由于预训练模型的文件大小较大(约 500MB),因此不会提供。通过遵循本教程,您将能够在几分钟内训练获得自己的训练模型。
使用 AAM 算法
完整的代码可在 face/samples/facemark_demo_aam.cpp 文件中找到。在本教程中,将介绍一些重要部分的解释。
创建 AAM 算法的实例
params.model_filename =
"AAM.yaml";
首先,创建 AAM 算法的参数实例。在本例中,我们将修改默认的缩放因子列表。默认情况下,使用的缩放因子为 1.0(无缩放)。这里我们添加了两个缩放因子,这将使实例以 2 和 4 的比例训练两个模型(分别缩小 2 倍和 4 倍,拟合时间更快)。但是,您应该确保此缩放因子不要太大,因为它会将图像缩放到非常小的图像。因此,它将丢失所有用于地标检测目的的重要信息。
或者,您可以像此示例一样覆盖默认缩放:
std::vector<float>scales;
scales.push_back(1.5);
scales.push_back(2.4);
FacemarkAAM::Params params;
params.scales = scales;
加载数据集
std::vector<String> images_train;
std::vector<String> landmarks_train;
loadDatasetList(images_path,annotations_path,images_train,landmarks_train);
数据集列表已加载到程序中。我们将在下一步中将数据集中的样本逐一放入。
将样本添加到训练器
std::vector<Point2f> facial_points;
for(size_t i=0;i<images_train.size();i++){
image =
imread(images_train[i].c_str());
facemark->addTrainingSample(image, facial_points);
}
来自数据集列表的图像以及其对应的注释数据逐一加载。然后将样本对添加到训练器。
训练过程
使用单行代码调用训练过程。确保所有需要的训练样本都已添加到训练器。
拟合准备
首先,您需要加载测试文件列表。
String testFiles(images_path), testPts(annotations_path);
if(!test_images_path.empty()){
testFiles = test_images_path;
testPts = test_images_path;
}
std::vector<String> images;
std::vector<String> facePoints;
由于 AAM 需要初始化参数(旋转、平移和缩放),因此您需要声明所需的变量来存储这些信息,这些信息将使用自定义函数获得。由于本例中 getInitialFitting() 函数的实现不是最优的,您可以创建自己的函数。
通过将训练模型的基准形状与当前人脸图像进行比较来获得初始化。在本例中,旋转是通过将输入人脸图像中两只眼睛形成的线的角度与基准形状中的同一条线进行比较来获得的。同时,缩放是通过将输入图像中眼睛之间的线的长度与基准形状进行比较来获得的。
拟合过程
拟合过程从检测给定图像中的人脸开始。
myDetector(image, faces, &face_cascade);
如果找到至少一张脸,则下一步是计算初始化参数。在本例中,由于 getInitialFitting() 函数不是最优的,因此它可能无法从给定的人脸上找到一对眼睛。因此,我们将过滤掉没有初始化参数的人脸,在本例中,conf 向量中的每个元素都表示每个过滤后的人脸的初始化参数。
std::vector<FacemarkAAM::Config> conf;
std::vector<Rect> faces_eyes;
for(unsigned j=0;j<faces.size();j++){
if(getInitialFitting(image,faces[j],s0,eyes_cascade, R,T,scale)){
faces_eyes.push_back(faces[j]);
}
}
对于存储在 conf 向量中的拟合参数,最后一个参数表示拟合过程中将使用的缩放因子的 ID。在本例中,拟合将使用最大的缩放因子 (4),预计与其他缩放相比,它的计算时间最快。如果 ID 大于模型中可用的训练比例,则使用具有最大比例 ID 的模型。
拟合过程非常简单,您只需要放入对应的图像,表示给定图像中所有面部的 ROI 的 cv::Rect 向量,由 landmarks 变量表示的地标点容器,以及配置变量。
if(conf.size()>0){
printf(" - 发现带有眼睛的面部 %i ", (int)conf.size());
std::vector<std::vector<Point2f> > landmarks;
facemark->fitConfig(image, faces_eyes, landmarks, conf);
for(unsigned j=0;j<landmarks.size();j++){
}
printf("%f ms\n",fittime*1000);
}else{
printf("初始化无法计算 - 跳过\n");
}
拟合过程完成后,您可以使用 drawFacemarks 函数可视化结果。