使用OpenCV和Dlib的頭部姿態(tài)估計
點(diǎn)擊上方“小白學(xué)視覺”,選擇加"星標(biāo)"或“置頂”
重磅干貨,第一時間送達(dá)
在許多應(yīng)用中,我們需要知道頭部相對于相機(jī)是如何傾斜的。例如,在虛擬現(xiàn)實(shí)應(yīng)用程序中,人們可以使用頭部的姿勢來呈現(xiàn)場景的正確視圖。在駕駛員輔助系統(tǒng)中,一個攝像頭可以用頭部姿態(tài)估計來判斷司機(jī)是否注意到了道路。當(dāng)然,我們可以使用基于頭部姿勢的手勢來控制一個沒有手的應(yīng)用程序/游戲。顯然,頭部姿勢估計在生活中是很有用的。

在計算機(jī)視覺中,物體的姿態(tài)是指物體相對于攝像機(jī)的相對方位和位置。可以通過相對于相機(jī)移動物體或相對于物體移動相機(jī)來改變姿勢。
本篇文章中描述的姿態(tài)估計問題通常稱為透視-n點(diǎn)計算機(jī)視覺術(shù)語中的問題或PNP。我們將在下面的章節(jié)中更詳細(xì)地看到,在這個問題中,我們的目標(biāo)是當(dāng)我們有一個校準(zhǔn)的照相機(jī)時,找到一個物體的姿態(tài),并且我們知道N物體上的三維點(diǎn)和圖像中相應(yīng)的2D投影。
三維剛性物體相對于攝像機(jī)只有兩種運(yùn)動方式。
1. 平移:將相機(jī)從其當(dāng)前的3D位置移動(X,Y,Z)到一個新的3D位置(X',Y',Z')。正如我們看到的,平移有3個自由度-你可以在X,Y或Z方向移動。翻譯用向量表示。t等于(X'-X,Y'-Y,Z'-Z)。
2. 旋轉(zhuǎn):我們還可以將攝像機(jī)旋轉(zhuǎn)到X,Y,Z軸。因此,旋轉(zhuǎn)也有三個自由度。有許多表示旋轉(zhuǎn)的方法。我們可以使用歐拉角(滾動、俯仰和偏航),a 旋轉(zhuǎn)矩陣,或旋轉(zhuǎn)方向(即軸)和角度.
因此,估計三維物體的姿態(tài)意味著找到6個數(shù)字--3個用于平移,3個用于旋轉(zhuǎn)。

要計算圖像中物體的三維姿態(tài),需要以下信息
1. 幾個點(diǎn)的二維坐標(biāo):我們需要二維(x,y)位置的幾個點(diǎn)的圖像。在上圖情況中,我們可以選擇眼角,鼻尖,嘴角等等。面部標(biāo)志探測器為我們提供了很多可供選擇的地方。在本篇文章中,我們將使用鼻尖,下巴,左眼的左角,右眼的右角,嘴角的左下角和嘴角。
2. 相同點(diǎn)的三維位置:我們還需要2D特征點(diǎn)的三維位置。你可能會想,我們需要照片中人的三維模型,以獲得三維位置。理想情況下,是的,但實(shí)際上并非如此。一個通用的3D模型就足夠了。你從哪里得到頭部的三維模型?我們不需要一個完整的3D模型。你只需要一些任意參照系中的幾個點(diǎn)的3D位置。在本篇文章中,我們將使用以下3D點(diǎn)。
鼻尖:(0.0,0.0,0.0)
下巴:(0.0,-330.0,-65.0)
左眼角:(-225.0f,170.0f,-135.0)
右眼角:(225.0,170.0,-135.0)
嘴角:(-150.0,-150.0,-125.0)
嘴角:(150.0,-150.0,-125.0)
請注意,上述各點(diǎn)位于某些任意的參考框架/坐標(biāo)系中。這叫做世界坐標(biāo)(OpenCV文檔中的模型坐標(biāo))。
3. 攝像機(jī)固有參數(shù)。如前所述,在這個問題中,相機(jī)被假定是校準(zhǔn)的。換句話說,我們需要知道相機(jī)的焦距,圖像中的光學(xué)中心和徑向畸變參數(shù)。所以我們需要校準(zhǔn)相機(jī)。我們可以用圖像的中心近似光學(xué)中心,近似焦距由圖像的寬度(以像素為單位)并假定徑向失真不存在。
姿態(tài)估計有幾種算法。第一個已知的算法可以追溯到1841年。解釋這些算法的細(xì)節(jié)超出了這篇文章的范圍,但是這里有一個一般性的想法。
這里有三個坐標(biāo)系。上述各種面部特征的三維坐標(biāo)如下所示世界坐標(biāo)。如果我們知道旋轉(zhuǎn)和平移(即姿態(tài)),我們就可以將世界坐標(biāo)中的3d點(diǎn)轉(zhuǎn)換為相機(jī)坐標(biāo)。相機(jī)坐標(biāo)中的三維點(diǎn)可以投影到圖像平面上。圖像坐標(biāo)系)利用攝像機(jī)的本征參數(shù)(焦距、光學(xué)中心等)。

由于一些原因,DLT解決方案不太準(zhǔn)確。第一,旋轉(zhuǎn)有三個自由度,但DLT解中使用的矩陣表示有9個數(shù)字。DLT解中沒有任何東西強(qiáng)迫估計的3×3矩陣為旋轉(zhuǎn)矩陣。更重要的是,DLT解決方案不會最小化正確的目標(biāo)函數(shù)。理想情況下,我們希望將重投影誤差如下所述。
如方程式所示2和3,如果我們知道正確的姿勢(和)通過將三維點(diǎn)投影到二維圖像上,可以預(yù)測三維人臉點(diǎn)在圖像上的二維位置。換句話說,如果我們知道和我們能找到重點(diǎn)在圖像中每一個3D點(diǎn).
我們還知道二維面部特征點(diǎn)(使用dlib或手動單擊)。我們可以觀察投影三維點(diǎn)和二維面部特征之間的距離。當(dāng)估計的姿態(tài)完美時,投影到圖像平面上的三維點(diǎn)幾乎與二維人臉特征一致。當(dāng)姿態(tài)估計不正確時,我們可以計算出再投影誤差測量-投影三維點(diǎn)和二維面部特征點(diǎn)之間的平方距離之和。
如前所述,對姿勢的大致估計(R和t)可以使用DLT解決方案找到。改進(jìn)DLT解決方案是隨機(jī)改變姿勢(R和t)并檢查重投影誤差是否減小。如果是的話,我們可以接受新的姿態(tài)估計。
在OpenCV中解決PnP和SolvePnPRansac可以用來估計姿勢。
解決PnP實(shí)現(xiàn)了幾種可以使用參數(shù)選擇的姿態(tài)估計算法。SOLVEPNP迭代它本質(zhì)上是DLT解,其次是Levenberg-Marquardt優(yōu)化。SOLVEPNP_P3P只使用一部分內(nèi)容來計算姿勢,只在使用時才調(diào)用。
在OpenCV 3中,引入了兩種新方法-SOLVEPNP_DLS和SOLVEPNP_UPnP。有趣的是SOLVEPNP_UPnP它也試圖估計攝像機(jī)的內(nèi)部參數(shù)。
SolvePnPRansac非常類似于解決PnP只是它用隨機(jī)樣本一致性(RANSAC)來準(zhǔn)確地估計姿勢。
當(dāng)我們懷疑一些數(shù)據(jù)噪聲比較大時,使用RANSAC是非常有用的。例如,考慮將一條線擬合到2D點(diǎn)的問題。這個問題可以用線性最小二乘法來解決,其中所有點(diǎn)離擬合線的距離都是最小的。現(xiàn)在來考慮一個糟糕的數(shù)據(jù)點(diǎn),這個數(shù)據(jù)點(diǎn)離我們很遠(yuǎn)。這一個數(shù)據(jù)點(diǎn)可以控制最小二乘解,我們對這條線的估計是非常錯誤的。在RANSAC中,參數(shù)是通過隨機(jī)選擇所需的最小點(diǎn)數(shù)來估計的。在一個直線擬合問題中,我們從所有數(shù)據(jù)中隨機(jī)選取兩個點(diǎn),并找到通過它們的直線。其他距離這條線足夠近的數(shù)據(jù)點(diǎn)被稱為“不動點(diǎn)”(Inliers)。通過隨機(jī)選取兩點(diǎn)得到直線的多個估計值,并選取最大誤差數(shù)的直線作為正確的估計。
使用SolvePnPRansac的參數(shù)如下所示。SolvePnPRansac都被解釋了。
C++*無效SolvePnPRansac(InputArray ObjectPoint,InputArray ImagePoint,InputArray CameraMatrix,InputArray disCoeffs,OutputArray rvec,OutputArray TVEC,bool useExtrinsicGuess=false,int iterationsCount=100,浮動重投影Error=8.0,int minInliersCount=100,OutputArray inliers=noArray(),int標(biāo)志=迭代)
Python:cv2.solvePnPRansac(ObjectPoint,ImagePoint,CameraMatrix,distCoeffs[,rvec[,tvc[,useExtrinsicGuess[,iterationsCount[,reprojectionError[,minInliersCount[,Inliers[,標(biāo)志])→rvec,tvc,inliers
迭代計數(shù)-挑選最低點(diǎn)數(shù)的次數(shù)和估計參數(shù)。重投射錯誤-正如前面在RANSAC中提到的,預(yù)測足夠接近的點(diǎn)稱為“不穩(wěn)定點(diǎn)”。這個參數(shù)值是觀測到的點(diǎn)投影和計算的點(diǎn)投影之間的最大允許距離,從而認(rèn)為它是一個獨(dú)立點(diǎn)。
MinInliersCount-不穩(wěn)定因素的數(shù)目。如果算法在某個階段發(fā)現(xiàn)了比minInliersCount更多的inliersCount,它就完成了。襯墊-輸出向量,其中包含ObtPoint和ImagePoint中的inliers索引。
OpenCV習(xí)慣于一種名為POSIT的姿態(tài)估計算法。它仍然存在于C API中(CvPosit),但不是C++API的一部分。PREST假設(shè)一個縮放的正射相機(jī)模型,因此你不需要提供焦距估計。這個函數(shù)現(xiàn)在已經(jīng)過時了,我們建議使用SolvePnp。我們提供了源程序代碼,感興趣的小伙伴們可以嘗試一下。
using namespace std;using namespace cv;int main(int argc, char **argv){// Read input imagecv::Mat im = cv::imread("headPose.jpg");// 2D image points. If you change the image, you need to change vectorstd::vector<cv::Point2d> image_points;image_points.push_back( cv::Point2d(359, 391) ); // Nose tipimage_points.push_back( cv::Point2d(399, 561) ); // Chinimage_points.push_back( cv::Point2d(337, 297) ); // Left eye left cornerimage_points.push_back( cv::Point2d(513, 301) ); // Right eye right cornerimage_points.push_back( cv::Point2d(345, 465) ); // Left Mouth cornerimage_points.push_back( cv::Point2d(453, 469) ); // Right mouth corner// 3D model points.std::vector<cv::Point3d> model_points;model_points.push_back(cv::Point3d(0.0f, 0.0f, 0.0f)); // Nose tipmodel_points.push_back(cv::Point3d(0.0f, -330.0f, -65.0f)); // Chinmodel_points.push_back(cv::Point3d(-225.0f, 170.0f, -135.0f)); // Left eye left cornermodel_points.push_back(cv::Point3d(225.0f, 170.0f, -135.0f)); // Right eye right cornermodel_points.push_back(cv::Point3d(-150.0f, -150.0f, -125.0f)); // Left Mouth cornermodel_points.push_back(cv::Point3d(150.0f, -150.0f, -125.0f)); // Right mouth corner// Camera internalsdouble focal_length = im.cols; // Approximate focal length.Point2d center = cv::Point2d(im.cols/2,im.rows/2);cv::Mat camera_matrix = (cv::Mat_<double>(3,3) << focal_length, 0, center.x, 0 , focal_length, center.y, 0, 0, 1);cv::Mat dist_coeffs = cv::Mat::zeros(4,1,cv::DataType<double>::type); // Assuming no lens distortioncout << "Camera Matrix " << endl << camera_matrix << endl ;// Output rotation and translationcv::Mat rotation_vector; // Rotation in axis-angle formcv::Mat translation_vector;// Solve for posecv::solvePnP(model_points, image_points, camera_matrix, dist_coeffs, rotation_vector, translation_vector);// Project a 3D point (0, 0, 1000.0) onto the image plane.// We use this to draw a line sticking out of the nosevector<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;// Display image.cv::imshow("Output", im);cv::waitKey(0);}
交流群
歡迎加入公眾號讀者群一起和同行交流,目前有SLAM、三維視覺、傳感器、自動駕駛、計算攝影、檢測、分割、識別、醫(yī)學(xué)影像、GAN、算法競賽等微信群(以后會逐漸細(xì)分),請掃描下面微信號加群,備注:”昵稱+學(xué)校/公司+研究方向“,例如:”張三 + 上海交大 + 視覺SLAM“。請按照格式備注,否則不予通過。添加成功后會根據(jù)研究方向邀請進(jìn)入相關(guān)微信群。請勿在群內(nèi)發(fā)送廣告,否則會請出群,謝謝理解~

