通过Dlib获得当前人脸的特征点,然后通过旋转平移标准模型的特征点进行拟合,计算标准模型求得的特征点与Dlib获得的特征点之间的差,使用Ceres不断迭代优化,最终得到最佳的旋转和平移参数。
使用环境系统环境:Ubuntu 18.04
使用语言:C++
编译工具:CMake
Dlib:用于获得人脸特征点
Ceres:用于进行非线性优化
CMinpack:用于进行非线性优化 (OPTIONAL)
https://github.com/Great-Keith/head-pose-estimation
基础概念 旋转矩阵头部的任意姿态可以转化为6个参数(yaw, roll, pitch, tx, ty, tz),前三个为旋转参数,后三个为平移参数。
平移参数好理解,原坐标加上对应的变化值即可;旋转参数需要构成旋转矩阵,三个参数分别对应了绕y轴旋转的角度、绕z轴旋转的角度和绕x轴旋转的角度。
具体代码实现我们可以通过Dlib已经封装好的API,rotate_around_x/y/z(angle)。该函数返回的类型是dlib::point_transform_affine3d,可以通过括号进行三维的变形,我们将其封装成一个rotate函数使用如下:
void rotate(std::vector<point3f>& points, const double yaw, const double pitch, const double roll) { dlib::point_transform_affine3d around_z = rotate_around_z(roll * pi / 180); dlib::point_transform_affine3d around_y = rotate_around_y(yaw * pi / 180); dlib::point_transform_affine3d around_x = rotate_around_x(pitch * pi / 180); for(std::vector<point3f>::iterator iter=points.begin(); iter!=points.end(); ++iter) *iter = around_z(around_y(around_x(*iter))); }[NOTE] 其中point3f是我自己定义的一个三维点坐标类型,因为Dlib中并没有提供,而使用OpenCV中的cv::Point3f会与dlib::point定义起冲突。定义如下:
typedef dlib::vector<double, 3> point3f;[NOTE] Dlib中的dlib::vector不是std::vector,注意二者区分。
LM算法这边不进行赘诉,建议跟着推导一遍高斯牛顿法,LM算法类似于高斯牛顿法的进阶,用于迭代优化求解非线性最小二乘问题。在该程序中使用Ceres/CMinpack封装好的API(具体使用见后文)。
三维空间到二维平面的映射根据针孔相机模型我们可以轻松的得到三维坐标到二维坐标的映射:
\(X^{2d}=f_x(\frac{X^{3d}}{Z^{3d}})+c_x\)
\(Y^{2d}=f_y(\frac{Y^{3d}}{Z^{3d}})+c_y\)
[NOTE] 使用上角标来表示3维坐标还是2维坐标,下同。
其中\(f_x, f_y, c_x, c_y\)为相机的内参,我们通过OpenCV官方提供的Calibration样例进行获取:
例如我的电脑所获得的结果如下:
从图中矩阵对应关系可以获得对应的参数值。
#define FX 1744.327628674942 #define FY 1747.838275588676 #define CX 800 #define CY 600[NOTE] 本程序不考虑外参。
具体步骤 获得标准模型的特征点该部分可见前一篇文章:BFM使用 - 获取平均脸模型的68个特征点坐标
我们将获得的特征点保存在文件 landmarks.txt 当中。
该部分不进行赘诉,官方有给出了详细的样例。
具体可以参考如下样例:
https://github.com/davisking/dlib/blob/master/examples/face_landmark_detection_ex.cpp
https://github.com/davisking/dlib/blob/master/examples/webcam_face_pose_ex.cpp(通过这个样例可以学习OpenCV如何调用摄像头)
其中使用官方提供的预先训练好的模型,下载地址:
具体在代码中使用如下:
cv::Mat temp; if(!cap.read(temp)) break; dlib::cv_image<bgr_pixel> img(temp); std::vector<rectangle> dets = detector(img); cout << "Number of faces detected: " << dets.size() << endl; std::vector<full_object_detection> shapes; for (unsigned long j = 0; j < dets.size(); ++j) { /* Use dlib to get landmarks */ full_object_detection shape = sp(img, dets[j]); /* ... */ }其中shape.part就存放着我们通过Dlib获得的当前人脸的特征点二维点序列。
[NOTE] 在最后CMake配置的时候,需要使用Release版本(最重要),以及增加选项USE_AVX_INSTRUCTIONS和USE_SSE2_INSTRUCTIONS/USE_SSE4_INSTRUCTIONS,否则因为Dlib的检测耗时较长,使用摄像头即时拟合会有严重的卡顿。
使用Ceres进行非线性优化Ceres的使用官方也提供了详细的样例,在此我们使用的是数值差分的方法,可参考:https://github.com/ceres-solver/ceres-solver/blob/master/examples/helloworld_numeric_diff.cc
Problem problem; CostFunction* cost_function = new NumericDiffCostFunction<CostFunctor, ceres::RIDDERS, LANDMARK_NUM, 6>(new CostFunctor(shape)); problem.AddResidualBlock(cost_function, NULL, x); Solver::Options options; options.minimizer_progress_to_stdout = true; Solver::Summary summary; Solve(options, &problem, &summary); std::cout << summary.BriefReport() << endl;