使用OpenCV和Dlib进行人头姿态估计

原文地址:http://www.learnopencv.com/head-pose-estimation-using-opencv-and-dlib/

       效果图:

使用OpenCV和Dlib进行人头姿态估计

在本教程中我们将学习如何估计人类的姿势使用OpenCV和Dlib照片。

在进行本教程之前,我想指出这个帖子属于我在面部处理中编写的一个系列。下面的一些文章有助于理解这篇文章,而其他文章补充了这一点。

1.脸部特征点检测

2.脸部变换

3.脸平均化

4.脸部变形

什么是姿势估计?

在计算机视觉中,物体的姿态是指相对于相机的相对取向和位置。您可以通过相对于相机移动对象或相机对象来更改姿势。

本教程中描述的姿态估计问题通常在计算机视觉术语中被称为透视n点问题或PNP。我们将在下面的章节中更详细地看到,在这个问题中,我们的目标是在我们有一个校准的相机时找到一个对象的姿态,并且我们知道对象上的n个3D点的位置和相应的2D投影图片。

如何以数学方式表示相机运动?

3D刚体对照相机只有两种运动。

(1)平移:将相机从当前的3D位置X,Y,Z)移动到新的3D位置X',Y',Z')被称为平移。你可以看到有3个*度——你可以在XYZ方向移动。向量tX'-X,Y'-Y,Z'-Z)表示。

(2)旋转:你也可以绕XYZ轴旋转相机。因此,旋转也具有三个*度。有许多表示旋转的方式,您可以使用:

1)欧拉角(roll滚动,pitch俯仰和yaw偏航)

使用OpenCV和Dlib进行人头姿态估计
roll
使用OpenCV和Dlib进行人头姿态估计
pitch
使用OpenCV和Dlib进行人头姿态估计
yaw

2)3x3旋转矩阵

3)旋转方向(即轴)和角度来表示它。

因此,估计3D对象的姿态意味着找到6个数字——3个用于平移,3个用于旋转。

你需要什么姿势估计?

使用OpenCV和Dlib进行人头姿态估计

要计算图像中对象的3D姿态,您需要以下信息:

(1)几个点的2D坐标:你需要在图像中几个点的2D(X,Y)的位置。在脸部的情况下,您可以选择眼睛的角落,鼻尖,嘴角等。Dlib的facial landmarkdetector为我们提供了很多选择。在本教程中,我们将使用鼻尖,下巴,左眼的左角,右眼的右角,嘴的左角和嘴的右角。

       (2)上述相同点的3D位置:您还需要2D特征点的3D位置。您可能会认为,您需要在照片中的人的3D模型来获取3D位置。理想中是的,但,实际上并不需要。通用的3D模型就足够了。从哪里得到一个头像的3D模型?好吧,你并非真的需要一个完整的3D模型。您仅仅需要在某些任意参考框架中的几个点的3D位置。在本教程中,我们将使用以下3D点:

       1)鼻尖:(0.0,0.0,0.0)

       2)下巴:(0.0,-330.0,-65.0)

       3)左眼左角:(-225.0f,170.0f,-135.0)

       4)右眼右角:( 225.0,170.0,-135.0)

       5)嘴角左侧:(-150.0,-150.0,-125.0)

6)嘴角右侧:(150.0,-150.0,-125.0)

请注意,以上几点在某些任意的参考帧/坐标系中。这被称为世界坐标系(a.k.a OpenCV文档中的模型坐标 )。

       (3)相机的内参数。如前所述,在这个问题中,假设相机被校准。换句话说,您需要知道相机的焦距,图像中的光学中心和径向失真参数。所以你需要校准你的相机。当然,对于我们之间的懒惰和愚蠢的人,这太多了。我可以提供捷径吗?当然,我可以!通过不使用准确的3D模型我们已经可以近似的去确定。我们可以通过图像的中心逼近光学中心,将焦距近似为像素的宽度,并假设不存在径向失真。Boom! you did not even have to get up from your couch!

姿态估计算法如何工作?

有几种姿态估计算法。第一个已知的算法可以追溯到1841年。来解释这些算法的细节已经超过这篇文章的范围,但它是一个一般的想法。

这里有三个坐标系。各种面部特征的3D坐标是建立在世界坐标系中。如果我们知道旋转和平移(即姿势),我们可以将世界坐标中的3D点变换为相机坐标中的3D点。可以使用相机的固有参数(焦距,光学中心等)将相机坐标中的3D点投影到图像平面(即图像坐标系)上。

使用OpenCV和Dlib进行人头姿态估计

我们来看看图像形成的方程,以了解上述坐标系的工作原理。在上图中,o是相机的中心,图中所示的平面是图像平面。我们有兴趣找出“什么样的方程可以将3D点p的投影P映射在图像平面上”。

假设我们知道世界坐标中3D点P的位置U,V,W)。假设我们知道旋转矩阵R(3x3矩阵)和平移t(3x1向量),他们都建立在相对于相机坐标系的世界坐标系中,我们可以使用以下公式计算摄像机坐标系中点P的位置X,Y,Z)

使用OpenCV和Dlib进行人头姿态估计

公式(1)

以扩展形式,上述方程如下所示:

使用OpenCV和Dlib进行人头姿态估计

公式(2)

如果你已经学习了线性代数,你会认识到,如果我们知道足够数量的点对应(即X,Y,Z)U,V,W)),上面是一个线性方程组。 和 是未知数,您可以轻松地解出未知数。

正如你将在下一节中看到的,我们知道X,Y,Z)只是一个未知的规模(阶),所以我们没有一个简单的线性系统。

直接线性变换

我们知道3D模型上的许多点(即U,V,W)),但是我们不知道X,Y,Z)。 我们只知道2D点的位置(即x,y))。 在没有径向变形的情况下,图像坐标中点p的坐标x,y)由下式给出:

使用OpenCV和Dlib进行人头姿态估计

公式(3)

其中, 和 是x和y方向上的焦距, 是光学中心。当涉及径向扭曲时,事情变得复杂得多,为了简单起见,我将其抛弃。

在方程式中的S呢?这是一个未知的比例因子。它存在于等式中,因为在任何图像中我们不知道图像的深度。如果将3D中的任何点P连接到相机的中心o,则光线与图像平面相交的点pP的图像。注意,沿着连接相机中心的点的所有点和点P产生相同的图像。换句话说,使用上述等式,您只能获得X,Y,Z)达到刻度s

现在这干扰了方程式2,因为它不再是我们知道如何解决的好的线性方程。我们的方程看起来更像:

使用OpenCV和Dlib进行人头姿态估计

公式(4)

幸运的是,使用一种称为直接线性变换(DirectLinear Transform,DLT)的方法,可以使用一些代数魔法解决上述形式的方程。只要您发现方程几乎是线性但是有一个未知比例的问题,您可以使用DLT。

Levenberg-Marquardt优化

上述DLT解决方案不是很准确,原因如下。首先,旋转R具有三个*度,但在DLT解决方案中使用的矩阵表示具有9个数字。DLT解决方案中没有任何内容迫使估计的3×3矩阵成为旋转矩阵。更重要的是,DLT解决方案不会使正确的目标函数最小化。理想情况下,我们希望最大限度地减少以下描述的重新投射错误(reprojection error)。

如等式2和3所示,如果我们知道正确的姿势(Rt),我们可以通过将3D点投影到图像上来预测图像上3D面部点的2D位置。换句话说,如果我们知道Rt,我们可以在每个3D点P的图像中找到点p

我们也知道2D面部特征点(使用Dlib或手动点击)。我们可以看看投影3D点和2D面部特征之间的距离。当估计的姿势是完美的,投影到图像平面上的3D点将几乎完美地与2D面部特征相匹配。当姿态估计不正确时,我们可以计算重投影误差量度——投影3D点与2D面部特征点之间的平方距离之和。

如前所述,可以使用DLT解决方案找到姿态的近似估计(Rt)。改善DLT解决方案的一个天真的方法是轻轻随意地改变姿势(Rt),并检查重新投射错误是否减少。如果是这样,我们可以接受新的姿势估计。我们可以一次又一次地保持扰乱Rt来找到更好的估计。虽然这个程序会奏效,但是会很慢。原来,有原则的方法迭代地改变Rt的值,以使重新投射错误减少。一种这样的方法称为Levenberg-Marquardt优化。查看*上的更多细节。

OpenCV solvePnP

在OpenCV中,函数solvePnPsolvePnPRansac可用于估计姿态。

solvePnP实现了几种用于姿态估计的算法,可以使用参数标志来选择。默认情况下,它使用标志SOLVEPNP_ITERATIVE,它本质上是DLT解决方案通过Levenberg-Marquardt优化。SOLVEPNP_P3P仅使用3点来计算姿势,只有在使用solvePnPRansac时才使用它。

在OpenCV 3中,引入了两种新的方法——SOLVEPNP_DLSSOLVEPNP_UPNP。关于SOLVEPNP_UPNP的有趣之处在于它也试图估计摄像机的内部参数。

C++: bool solvePnP(InputArrayobjectPoints, InputArray imagePoints, InputArray cameraMatrix, InputArraydistCoeffs, OutputArray rvec, OutputArray tvec, bool useExtrinsicGuess=false,int flags=SOLVEPNP_ITERATIVE )

Parameters:

       objectPoints - 世界坐标空间中的对象点数组。我通常通过N3D点的向量。您还可以传递大小为Nx3(或3xN)单通道矩阵,或Nx1(或1xN3通道矩阵的Mat我强烈推荐使用矢量。

imagePoints - 对应图像点的数组。你应该传递一个N 2D点的向量。但您也可以通过2xN(或Nx21通道或1xN(或Nx12通道垫,其中N是点数。

cameraMatrix - 输入相机矩阵A = 注意,在某些情况下, 可以通过像素的图像宽度来近似,并且 可以是图像中心的坐标。

distCoeffs - 45812个元素的失真系数 [ [ ][ ]]的输入向量。如果向量为空/空,则假定零失真系数。除非您正在使用像变形巨大的Go-Pro像相机,否则我们可以将其设置为NULL如果您正在使用高失真镜头,建议您进行全面的相机校准。

rvec - 输出旋转矢量。

tvec - 输出平移向量。

useExtrinsicGuess - 用于SOLVEPNP_ITERATIVE的参数。如果为真(1),则函数使用提供的rvectvec值作为旋转和平移向量的初始近似值,并进一步优化它们。

Method for solving a PnP problem

SOLVEPNP_ITERATIVE迭代法基于Levenberg-Marquardt优化。在这种情况下,该功能可以找到这样一种姿态,使重播误差最小化,即观察到的投影图像点与投影(使用projectPoints())对象点之间的距离之间的平方和之和。

SOLVEPNP_P3P方法是基于X.S.的论文。高,X.-R HouJ. TangH.-F. Chang“三点问题的完整解决方案分类在这种情况下,该功能只需要四个对象和图像点。

SOLVEPNP_EPNP方法由F.Moreno-NoguerV.LepetitP.Fua在论文“EPnPEfficient Perspective-n-Point Camera Pose Estimation”中引入。

以下标志仅适用于OpenCV 3

SOLVEPNP_DLS方法基于Joel A. HeschStergios I. Roumeliotis的论文。 “PnP的直接最小二乘法(DLS)方法

SOLVEPNP_UPNP方法基于A.Penate-SanchezJ.Andrade-CettoM.Moreno-Noguer的论文。用于强大的相机姿态和焦距估计的穷尽线性化在这种情况下,假设两者都具有相同的值,函数也估计参数f_xf_y然后用估计的焦距更新cameraMatrix

OpenCV solvePnPRansac

solvePnPRansac与solvePnP非常相似,只是它使用随机样本一致性(RANSAC)来鲁棒估计姿势。

当您怀疑几个数据点非常嘈杂时,使用RANSAC非常有用。例如,考虑将线拟合到2D点的问题。使用线性最小二乘法可以解决这个问题,其中从拟合线的所有点的距离最小化。现在考虑一个非常糟糕的数据点。这一个数据点可以控制最小二乘解决方案,我们对该行的估计将是非常错误的。在RANSAC中,通过随机选择最小点数来估计参数。在线拟合问题中,我们从所有数据中随机选择两个点,并找到通过它们的线。距离线路足够近的其他数据点称为内联。通过随机选择两个点来获得线的几个估计,并且选择具有最大数目的线内值的线作为正确估计。

solvePnPRansac的使用如下所示,并解释了对于solpnPRansac特定的参数。

C++:void solvePnPRansac(InputArrayobjectPoints, InputArray imagePoints, InputArray cameraMatrix, InputArraydistCoeffs, OutputArray rvec, OutputArray tvec, bool useExtrinsicGuess=false,int iterationsCount=100, float reprojectionError=8.0, int minInliersCount=100,OutputArray inliers=noArray(), int flags=ITERATIVE )

iterationsCount - 选择最小点数和估计参数的次数。

reprojectionError - 如前所述,在RANSAC中,预测足够近的点被称为“内在”。该参数值是观测值和计算点投影之间的最大允许距离,以将其视为一个惰性。

minInliersCount - 内联数。如果在某个阶段的算法比minInliersCount发现更多的内核,它会完成。

inliers - 包含objectPointsimagePoints中的内联索引的输出向量。

OpenCV POSIT

OpenCV用于称为POSIT的姿态估计算法。 它仍然存在于C的API(cvPosit)中,但不是C ++API的一部分。 POSIT假设一个缩放的正交相机模型,因此您不需要提供焦距估计。此功能现在已经过时了,我建议您使用solvePnp中实现的一种算法。

OpenCV姿势估计代码:C ++ / Python

在本节中,我在C ++和Python*享了一个示例代码,用于单个图像中的头部姿态估计。您可以在这里下载图片headPose.jpg

面部特征点的位置是硬编码(设置好的)的,如果要使用自己的图像,则需要更改矢量image_points(特征点,上面说的下巴、眼睛、鼻尖等)。

#include <opencv2/opencv.hpp>

 

using namespace std;

using namespace cv;

 

int main(int argc, char **argv)

{

     

    // Read inputimage

    cv::Mat im =cv::imread("headPose.jpg");

     

    // 2D imagepoints. If you change the image, you need to change vector

    std::vector<cv::Point2d>image_points;

    image_points.push_back(cv::Point2d(359, 391) );    // Nose tip

    image_points.push_back(cv::Point2d(399, 561) );    // Chin

    image_points.push_back(cv::Point2d(337, 297) );     // Left eye left corner

    image_points.push_back(cv::Point2d(513, 301) );    // Right eye right corner

    image_points.push_back(cv::Point2d(345, 465) );    // Left Mouth corner

    image_points.push_back(cv::Point2d(453, 469) );    // Right mouth corner

     

    // 3D modelpoints.

    std::vector<cv::Point3d>model_points;

    model_points.push_back(cv::Point3d(0.0f,0.0f,0.0f));              // Nose tip

    model_points.push_back(cv::Point3d(0.0f,-330.0f, -65.0f));          //Chin

    model_points.push_back(cv::Point3d(-225.0f,170.0f, -135.0f));       // Left eye left corner

    model_points.push_back(cv::Point3d(225.0f,170.0f, -135.0f));        // Right eye rightcorner

    model_points.push_back(cv::Point3d(-150.0f,-150.0f, -125.0f));      // Left Mouth corner

    model_points.push_back(cv::Point3d(150.0f,-150.0f, -125.0f));       // Right mouth corner

     

    // Camerainternals

    doublefocal_length = im.cols; // Approximate focal length.

    Point2d center =cv::Point2d(im.cols/2,im.rows/2);

    cv::Matcamera_matrix = (cv::Mat_<double>(3,3) << focal_length, 0,center.x, 0 , focal_length, center.y, 0, 0, 1);

    cv::Matdist_coeffs = cv::Mat::zeros(4,1,cv::DataType<double>::type); // Assumingno lens distortion

     

    cout <<"Camera Matrix " << endl << camera_matrix << endl ;

    // Outputrotation and translation

    cv::Matrotation_vector; // Rotation in axis-angle form

    cv::Mattranslation_vector;

     

    // Solve forpose

    cv::solvePnP(model_points,image_points, camera_matrix, dist_coeffs, rotation_vector, translation_vector);

 

     

    // Project a 3Dpoint (0, 0, 1000.0) onto the image plane.

    // We use thisto draw a line sticking out of the nose

     

    vector<Point3d>nose_end_point3D;

    vector<Point2d>nose_end_point2D;

    nose_end_point3D.push_back(Point3d(0,0,1000.0));

     

    projectPoints(nose_end_point3D,rotation_vector, translation_vector, camera_matrix, dist_coeffs,nose_end_point2D);

     

     

    for(int i=0; i< image_points.size(); i++)

    {

        circle(im,image_points[i], 3, Scalar(0,0,255), -1);

    }

     

    cv::line(im,image_points[0],nose_end_point2D[0], cv::Scalar(255,0,0), 2);

     

    cout <<"Rotation Vector " << endl << rotation_vector <<endl;

    cout <<"Translation Vector" << endl << translation_vector<< endl;

     

    cout<<  nose_end_point2D << endl;

     

    // Displayimage.

    cv::imshow("Output",im);

    cv::waitKey(0);

 

}

 

使用Dlib实时姿态估计

这篇文章中包含的视频是使用我的dlib分支,可以免费为这个博客的订阅者使用。如果您已经订阅,请查看欢迎电子邮件链接到我的dlib fork,并查看此文件。

dlib/examples/webcam_head_pose.cpp

Dlib中获取的各个点

std::vectorget_2d_image_points(full_object_detection &d)
{
std::vector image_points;
image_points.push_back( cv::Point2d(d.part(30).x(), d.part(30).y() ) ); // Nose tip
image_points.push_back( cv::Point2d(d.part(8).x(), d.part(8).y() ) ); // Chin
image_points.push_back( cv::Point2d(d.part(36).x(), d.part(36).y() ) ); // Left eye left corner
image_points.push_back( cv::Point2d(d.part(45).x(), d.part(45).y() ) ); // Right eye right corner
image_points.push_back( cv::Point2d(d.part(48).x(), d.part(48).y() ) ); // Left Mouth corner
image_points.push_back( cv::Point2d(d.part(54).x(), d.part(54).y() ) ); // Right mouth corner
return image_points;

}