1. 程式人生 > >使用OpenCV和Dlib進行人頭姿態估計

使用OpenCV和Dlib進行人頭姿態估計

       效果圖:

在本教程中我們將學習如何估計人類的姿勢使用OpenCV和Dlib照片。

在進行本教程之前,我想指出這個帖子屬於我在面部處理中編寫的一個系列。下面的一些文章有助於理解這篇文章,而其他文章補充了這一點。

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偏航)

roll
pitch
yaw

2)3x3旋轉矩陣

因此,估計3D物件的姿態意味著找到6個數字——3個用於平移,3個用於旋轉。

你需要什麼姿勢估計?

要計算影象中物件的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點投影到影象平面(即影象座標系)上。

我們來看看影象形成的方程,以瞭解上述座標系的工作原理。在上圖中,o是相機的中心,圖中所示的平面是影象平面。我們有興趣找出“什麼樣的方程可以將3D點p的投影P對映在影象平面上”。

假設我們知道世界座標中3D點P的位置U,V,W)。假設我們知道旋轉矩陣R(3x3矩陣)和平移t(3x1向量),他們都建立在相對於相機座標系的世界座標系中,我們可以使用以下公式計算攝像機座標系中點P的位置X,Y,Z)

公式(1)

以擴充套件形式,上述方程如下所示:

公式(2)

如果你已經學習了線性代數,你會認識到,如果我們知道足夠數量的點對應(即X,Y,Z)U,V,W)),上面是一個線性方程組。 和 是未知數,您可以輕鬆地解出未知數。

正如你將在下一節中看到的,我們知道X,Y,Z)只是一個未知的規模(階),所以我們沒有一個簡單的線性系統。

直接線性變換

我們知道3D模型上的許多點(即U,V,W)),但是我們不知道X,Y,Z)。 我們只知道2D點的位置(即x,y))。 在沒有徑向變形的情況下,影象座標中點p的座標x,y)由下式給出:

公式(3)

其中, 和 是x和y方向上的焦距, 是光學中心。當涉及徑向扭曲時,事情變得複雜得多,為了簡單起見,我將其拋棄。

在方程式中的S呢?這是一個未知的比例因子。它存在於等式中,因為在任何影象中我們不知道影象的深度。如果將3D中的任何點P連線到相機的中心o,則光線與影象平面相交的點pP的影象。注意,沿著連線相機中心的點的所有點和點P產生相同的影象。換句話說,使用上述等式,您只能獲得X,Y,Z)達到刻度s

現在這干擾了方程式2,因為它不再是我們知道如何解決的好的線性方程。我們的方程看起來更像:

公式(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;

}