写在前面
经过两周的文献和博客阅读,CV_Life君终于欣(dan)喜(zhan)若(xin)狂(jing)地给各位带来head pose estimation这篇文章,因为刚刚入手这个方向,如有疏漏请各位多多包涵,并多多指教。废话少说,先放个Demo热热身。
Head Pose Estimation是干啥的?
热身完毕,有没有对Demo上变化的数字费解呢?做过此方向的小伙伴,应该会比较容易理解,Head Pose Estimation 就是估计头部的姿态。详细道来:Head Pose Estimation 是通过一幅面部图像来获得头部的姿态角,跟飞机飞行有点类似,即计算 pitch,yaw 和 roll 三个欧拉角,分别学名俯仰角、偏航角和滚转角,通俗讲就是抬头、摇头和转头。百闻不如一见,上图示意
Head Pose Estimation有啥用呢?
记得在群里问“群里有没有做过 Head Pose Estimation 研究的小伙伴?”,有人问过“这个目的是什么?”。其实 Head Pose Estimaion 的应用场景和目的挺丰富的,下面CV_Life君就跟小伙伴们分享几个方向。
(1) 注意力检测。CV_Life君目前就在做这个方向,通过判断头部姿态可以判断人的注意力情况。比如可以检测长途司机是不是在目视前方,长时间不目视前方的话,可以提前敲打,保证安全,减少事故;再比如监控学生上课时是否集中精力,以后再也不用担心班主任在后窗偷窥了。
(2) 行为分析。和上面的有点类似,但还是有点不同。我家乡方言里有个词叫“胡撒”,说的就是心虚的人容易左顾右盼,通过视频监控分析再辅助其他算法可以判断一个人是否具有不轨行为,做到提前预警,防患于未然。
(3) 人机互动。人的头部动作有时可以表示意义,传递信息。摇头在大多数人看来是否认,点头表示同意(三哥表示不服),长时间低头说不定你就是“地狱之门”的沉思者。如果机器人能理解这样的行为,将提高人机交互的质量和有效性。
(4) 视线追踪,也可以称为眼球跟踪。准确的 Head Pose Estimation 能够提高视线追踪的精度。视线追踪可以用在游戏领域,也许有一天你打开手游后,用眼睛就可以控制游戏内人物的移动了(体验如何暂且不管,要的是黑科技),让体感操作更上一层楼。
说完了 Head Pose Estimation 的八卦,既然这玩意这么有用,小伙伴们是不是已经迫不及待地想去试试手呢?下面CV_Life君就说说 Head Pose Estimation 的原理之一。
Head Pose Estimation 如何理解?
如果你对相机标定熟悉的话,就比较好理解,因为 Head Pose Estimation 比较有难度的部分已经被大牛们搞定了,CV_Life君普及一下比较基本的原理。一种比较经典的 Head Pose Estimation 算法的步骤一般为:2D人脸关键点检测;3D人脸模型匹配;求解3D点和对应2D点的转换关系;根据旋转矩阵求解欧拉角。Bingo!就是这么简单。
下面是原理时间。众所周知一个物体相对于相机的姿态可以使用旋转矩阵和平移矩阵来表示。
平移矩阵:物体相对于相机的空间位置关系矩阵,用T表示;
旋转矩阵:物体相对于相机的空间姿态关系矩阵,用R表示。
如此看来必然少不了坐标系转换。讲点人性,继续上图
于是世界坐标系(UVW)、相机坐标系(XYZ)、图像中心坐标系(uv)和像素坐标系(xy)四兄弟闪亮登场。如果相机完美无瑕,老三可以回家洗洗睡觉,关系也相对简单。
世界坐标系到相机坐标系:
相机坐标系到像素坐标系:
因此像素坐标系和世界坐标系的关系如下:
上式的求解可用DLT(Direct Linear Transform)算法结合最小二乘进行迭代求解,最小二乘的目标函数可为
最后图像中心坐标系到像素坐标系:
看来只要知道世界坐标系内点的位置、像素坐标位置和相机参数就可以搞定旋转和平移矩阵,可上面的关系分明是非线性的,这可怎么解啊?其实OpenCV已经给我们提供了求解PnP问题的函数solvePnp(),一步轻松到位。
可能又有小伙伴举手了:2D关键点怎么检测啊?这个咱这里就不讨论了,有兴趣自行google,因为CV_Life君目前也没研究明白(捂脸),不过还好有大牛贡献源代码,咱们先行尝鲜,后续再去慢慢品尝。
人脸3D点和2D点的对应关系如下所示,目前的算法可以检测到更多的关键点,比如商汤科技的关键点检测已经可以做到240,可谓行业佼佼者。下面代码用到的是人脸68点检测算法。
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/calib3d/calib3d.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include "../dlib-19.16/dlib/opencv.h"
#include "../dlib-19.16/dlib/image_processing/frontal_face_detector.h"
#include "../dlib-19.16/dlib/image_processing/render_face_detections.h"
#include "../dlib-19.16/dlib/image_processing.h"
#include <iostream>
#include <vector>
double K[9] = { 6.5308391993466671e+002, 0.0, 3.1950000000000000e+002, 0.0, 6.5308391993466671e+002, 2.3950000000000000e+002, 0.0, 0.0, 1.0 }; // 等价于矩阵[fx, 0, cx; 0, fy, cy; 0, 0, 1]
double D[5] = { 7.0834633684407095e-002, 6.9140193737175351e-002, 0.0, 0.0, -1.3073460323689292e+000 }; // 相机畸变参数[k1, k2, p1, p2, k3]
dlib::frontal_face_detector detector = dlib::get_frontal_face_detector(); // 人脸检测模型
dlib::shape_predictor predictor; // 关键点检测模型
dlib::deserialize("shape_predictor_68_face_landmarks.dat") >> predictor; // 68关键点检测
std::vector<cv::Point3d> object_pts; // 空间点坐标集合 model referenced from http://aifi.isr.uc.pt/Downloads/OpenGL/glAnthropometric3DModel.cpp
std::vector<cv::Point2d> image_pts; // 像素坐标集合
std::vector<dlib::rectangle> faces = detector(cimg); // 检测人脸,cimg为摄像头拍摄的图像或视频帧图像
if(faces.size() > 0) {
dlib::full_object_detection shape = predictor(cimg, faces[0]); // 检测第一个人脸的关键点
......
}
cv::solvePnP(object_pts, image_pts, cam_matrix, dist_coeffs, rotation_vec, translation_vec); // cam_matrix与K矩阵对应,dist_coeffs与D矩阵对应,rotation_vec表示旋转矩阵,translation_vec表示平移矩阵
cv::Rodrigues(rotation_vec, rotation_mat);
cv::hconcat(rotation_mat, translation_vec, pose_mat);
cv::decomposeProjectionMatrix(pose_mat, out_intrinsics, out_rotation, out_translation, cv::noArray(), cv::noArray(), cv::noArray(), euler_angle);
说了这么多,CV_Life君终于啰嗦完毕,希望有心读完的小伙伴对 Head Pose Estimation 能有一定的理解。当然 Head Pose Estimation 的算法还有很多,后续CV_Life君准备研究一下机器学习和深度学习的方法,有兴趣的可以一起讨论学习。