目标
在本教程中,你将学习如何
- 创建 FacemarkAAM 实例
- 训练 AAM 模型
- 使用 FacemarkAAM 进行拟合
准备
在继续本教程之前,你应该下载人脸特征点检测数据集。我们建议你下载 LFPW 数据集,该数据集可以在 https://ibug.doc.ic.ac.uk/download/annotations/lfpw.zip 获取。
确保 API 支持注释格式,注释文件中的内容应如下例所示
版本:1
点数: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 算法实例
FacemarkAAM::Params params;
params.scales.push_back(2.0);
params.scales.push_back(4.0);
params.model_filename = "AAM.yaml";
Ptr<FacemarkAAM> facemark = FacemarkAAM::create(params);
首先,创建一个用于 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);
数据集列表已加载到程序中。我们将在下一步逐个加载数据集中的样本。
将样本添加到训练器
Mat image;
std::vector<Point2f> facial_points;
for(size_t i=0;i<images_train.size();i++){
image = imread(images_train[i].c_str());
loadFacePoints(landmarks_train[i],facial_points);
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;
loadDatasetList(testFiles, testPts, images, facePoints);
由于 AAM 需初始化参数(旋转、平移和缩放),因此您需要声明所需变量来存储这些信息,该信息将通过自定义函数获取。由于本示例中 getInitialFitting() 函数的实现不是最佳的,因此您可以创建自己的函数。
通过比较训练模型的基本形状和当前人脸图像进行初始化。在本例中,通过比较输入人脸图像中两眼神线所形成的线与基本形状中的同一条线的角度来获得旋转。同时,通过比较输入图像中两眼神线的长度和基本形状来获得缩放。
拟合过程
该拟合过程首先检测给定图像中的人脸。
image = imread(images[i]);
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)){
conf.push_back(FacemarkAAM::Config(R,T,scale,(int)params.scales.size()-1));
faces_eyes.push_back(faces[j]);
}
}
对于存储在 conf
向量中的拟合参数,最后一个参数表示拟合过程中使用的缩放因子 ID。在本示例中,拟合将使用最大的缩放因子 (4),与其他比例相比,它的计算时间预计最快。如果 ID 大于模型中可用的训练比例,则使用比例 ID 最大的模型。
拟合过程非常简单,你只需放如相应图像、表示给定图像中所有面部感兴趣区域的 cv::Rect
向量、表示地标点(由 landmarks
变量表示)的容器以及配置变量。
if(conf.size()>0){
printf(" - 面部与眼睛一起发现 %i ", (int)conf.size());
std::vector<std::vector<Point2f> > landmarks;
double newtime = (double)getTickCount();
facemark->fitConfig(image, faces_eyes, landmarks, conf);
double fittime = ((getTickCount() - newtime)/getTickFrequency());
for(unsigned j=0;j<landmarks.size();j++){
drawFacemarks(image, landmarks[j],Scalar(0,255,0));
}
printf("%f ms\n",fittime*1000);
imshow("拟合", image);
waitKey(0);
}else{
printf("无法计算初始化 - 跳过\n");
}
拟合过程完成后,你可以使用 drawFacemarks
函数视结果。