Open ysh329 opened 5 years ago
具体代码说明,见前文内容。这部分是对前文内容基于LM方法的具体实现,LM方法调用OpenCV的solvePnP
接口:
import cv2
import numpy as np
import math
def get_angle(self, img, five_points_dict):
img_size = img.shape
if type(five_points_dict)==tuple or five_points_dict==None:
print("[ERROR] no face found or many faces")
return None
image_points = np.array([five_points_dict['nose'],\
five_points_dict['left_eye'],\
five_points_dict['right_eye'],\
five_points_dict['mouse_left'],\
five_points_dict['mouse_right'],\
])
# 3D model points.
model_points = np.array([
(0.0, 0.0, 0.0), # Nose tip
(-165.0, 170.0, -135.0), # Left eye left corner
(165.0, 170.0, -135.0), # Right eye right corner
(-150.0, -150.0, -125.0), # Left Mouth corner
(150.0, -150.0, -125.0) # Right mouth corner
])
# Camera internals
focal_length = img_size[1]
center = (img_size[1] / 2, img_size[0] / 2)
camera_matrix = np.array(
[[focal_length, 0, center[0]],
[0, focal_length, center[1]],
[0, 0, 1]], dtype="double"
)
dist_coeffs = np.zeros((4, 1)) # Assuming no lens distortion
(success, rotation_vector, translation_vector) = cv2.solvePnP(model_points,\
image_points,\
camera_matrix,\
dist_coeffs,\
flags=cv2.CV_ITERATIVE)
# calculate rotation angles
theta = cv2.norm(rotation_vector)
# transformed to quaterniond
w = np.cos(theta / 2)
x = np.sin(theta / 2) * rotation_vector[0] / theta
y = np.sin(theta / 2) * rotation_vector[1] / theta
z = np.sin(theta / 2) * rotation_vector[2] / theta
# quaterniondToEulerAngle
ysqr = y * y
xsqr = x * x
zsqr = z * z
# pitch (x-axis rotation)
t0 = 2.0 * (w * x + y * z)
t1 = 1.0 - 2.0 * (xsqr + ysqr)
pitch = math.atan2(t0, t1)
pitch = pitch * 180 / math.pi
# yaw (y-axis rotation)
t2 = 2.0 * (w * y - z * x)
t2 = 1.0 if t2 > 1.0 else t2
t2 = -1.0 if t2 < -1.0 else t2
yaw = math.asin(t2)
yaw = yaw * 180 / math.pi
# roll (z-axis rotation)
t3 = 2.0 * (w * z + x * y)
t4 = 1.0 - 2.0 * (ysqr + zsqr)
roll = math.atan2(t3, t4)
roll = roll * 180 / math.pi
if roll > 90:
roll = (roll - 180) % 180
if roll < -90:
roll = (roll + 180) % 180
angle_dict = {"pitch": pitch,\
"yaw": yaw,\
"roll": roll}
return angle_dict
最新姿态估计研究进展,一般分为两种:
deepgaze 研究头部姿态和注意力方向。主要为头部姿态估计 先框出人脸区域,再进行姿态估计。
注:本节的整体脉络来自这篇博文:Levenberg-Marquardt算法浅谈 | liu14lang,在此我加以补充和扩写。
讲Levenberg-Marquardt算法前,先有必要讲一下牛顿法(Newton's method )和高斯牛顿法(Quasi-Newton Methods)。
牛顿法(Newton's method)
牛顿法是一种在实数域和复数域上近似求解方程的方法。方法使用函数
f(x)
的泰勒级数(Taylor series,用无限项连加式——级数,来表示一个函数,这些相加的项由函数在某一点的导数求得)的前面几项来寻找方程f (x) = 0
的根。牛顿法最大的特点就在于它的收敛速度很快(二次收敛)。具体步骤
首先,选择一个接近函数
f (x)
零点的x0
,计算相应的f (x0)
和切线斜率f ' (x0)
(这里f '
表示函数f
的导数)。然后我们计算穿过点(x0, f (x0))
并且斜率为f '(x0)
的直线和 x 轴的交点的x坐标,也就是求如下方程的解: 上面公式不好想的话,可以这么想:我们已知过点(x0, f(x0))
、斜率为f'(x0)
、但不知截距的直线,那么列直线方程可以求得截距,得到截距后就得到了该直线,然后再计算该直线和 x 轴的交点的 x 坐标。那就设y = 0
,求 x即 可。这时便有该直线与 x 轴的交点坐标。 将上述公式左右两边同时除以f'(x0)
,就得到x + f(x0)/f‘’(x0) - x0= 0
,经过移动,即得到x = x0 - f(x0)/f'(x0)
,即下面的公式(见下图); 将新求得的点的 x 坐标命名为x1
,通常x1
会比x0
更接近方程f (x) = 0
的解。因此我们现在可以利用x1
开始下一轮迭代。迭代公式可化简为如下所示:
- 点斜式方程_百度百科 已经有人证明了,若
f '
是连续的,且待求的零点x
是孤立的,那么在零点x
周围存在一个区域,只要初始值x0
位于这个邻近区域内,那么牛顿法必定收敛。并且,若f ' (x)
不为0,那么牛顿法将具有平方收敛的性能。粗略地说,这意味着每迭代一次,牛顿法结果的有效数字将增加一倍(见下图,牛顿法在二维情况下搜索路径的动态过程)。由于牛顿法是基于当前位置的切线来确定下一次的位置,所以牛顿法又被很形象地称为是“切线法”。关于牛顿法和梯度下降法的效率对比
本质上看,牛顿法是二阶收敛,梯度下降是一阶收敛,所以牛顿法就更快。如果更通俗地说的话,比如你想找一条最短的路径走到一个盆地的最底部,梯度下降法每次只从你当前所处位置选一个坡度最大的方向走一步,牛顿法在选择方向时,不仅会考虑坡度是否够大,还会考虑你走了一步之后,坡度是否会变得更大。所以,可以说牛顿法比梯度下降法看得更远一点,能更快地走到最底部。(牛顿法目光更加长远,所以少走弯路;相对而言,梯度下降法只考虑了局部的最优,没有全局思想。) 根据wiki上的解释,从几何上说,牛顿法是用一个二次曲面去拟合你当前所处位置的局部曲面,而梯度下降法是用一个平面去拟合当前的局部曲面,通常情况下,二次曲面的拟合会比平面更好,所以牛顿法选择的下降路径会更符合真实的最优下降路径。 注:红色的牛顿法的迭代路径,绿色的是梯度下降法的迭代路径。
牛顿法的优缺点总结
- 优点:二阶收敛,收敛速度快;
- 缺点:牛顿法是一种迭代算法,每一步都需要求解目标函数的Hessian矩阵的逆矩阵,计算比较复杂。
【】【】【】【】【】【】【】【】【】【】【】【】【】
Newton method · Issue #22 · yuenshome/yuenshome.github.io https://github.com/yuenshome/yuenshome.github.io/issues/22
其是实现的solvePnP
函数opencv/solvepnp.cpp at 8f15a609afc3c08ea0a5561ca26f1cf182414ca2 · opencv/opencv
如下:
bool solvePnP( InputArray _opoints, InputArray _ipoints,
InputArray _cameraMatrix, InputArray _distCoeffs,
OutputArray _rvec, OutputArray _tvec, bool useExtrinsicGuess, int flags )
{
CV_INSTRUMENT_REGION();
Mat opoints = _opoints.getMat(), ipoints = _ipoints.getMat();
int npoints = std::max(opoints.checkVector(3, CV_32F), opoints.checkVector(3, CV_64F));
CV_Assert( ( (npoints >= 4) || (npoints == 3 && flags == SOLVEPNP_ITERATIVE && useExtrinsicGuess) )
&& npoints == std::max(ipoints.checkVector(2, CV_32F), ipoints.checkVector(2, CV_64F)) );
Mat rvec, tvec;
if( flags != SOLVEPNP_ITERATIVE )
useExtrinsicGuess = false;
if( useExtrinsicGuess )
{
int rtype = _rvec.type(), ttype = _tvec.type();
Size rsize = _rvec.size(), tsize = _tvec.size();
CV_Assert( (rtype == CV_32F || rtype == CV_64F) &&
(ttype == CV_32F || ttype == CV_64F) );
CV_Assert( (rsize == Size(1, 3) || rsize == Size(3, 1)) &&
(tsize == Size(1, 3) || tsize == Size(3, 1)) );
}
else
{ // 省略................................... }
rvec = _rvec.getMat();
tvec = _tvec.getMat();
Mat cameraMatrix0 = _cameraMatrix.getMat();
Mat distCoeffs0 = _distCoeffs.getMat();
Mat cameraMatrix = Mat_<double>(cameraMatrix0);
Mat distCoeffs = Mat_<double>(distCoeffs0);
bool result = false;
if (flags == SOLVEPNP_EPNP || flags == SOLVEPNP_DLS || flags == SOLVEPNP_UPNP)
{ // 省略................................... }
else if (flags == SOLVEPNP_P3P)
{ // 省略................................... }
else if (flags == SOLVEPNP_AP3P)
{ // 省略................................... }
else if (flags == SOLVEPNP_ITERATIVE)
{
CvMat c_objectPoints = cvMat(opoints), c_imagePoints = cvMat(ipoints);
CvMat c_cameraMatrix = cvMat(cameraMatrix), c_distCoeffs = cvMat(distCoeffs);
CvMat c_rvec = cvMat(rvec), c_tvec = cvMat(tvec);
cvFindExtrinsicCameraParams2(&c_objectPoints, &c_imagePoints, &c_cameraMatrix,
(c_distCoeffs.rows && c_distCoeffs.cols) ? &c_distCoeffs : 0,
&c_rvec, &c_tvec, useExtrinsicGuess );
result = true;
}
/*else if (flags == SOLVEPNP_DLS)
{ // 省略................................... }
else if (flags == SOLVEPNP_UPNP)
{ // 省略................................... }*/
else
CV_Error(CV_StsBadArg, "The flags argument must be one of SOLVEPNP_ITERATIVE, SOLVEPNP_P3P, SOLVEPNP_EPNP or SOLVEPNP_DLS");
return result;
}
Levenberg-Marquardt优化算法的实现是其中的flags == SOLVEPNP_ITERATIVE
的情况,该情况下调用了cvFindExtrinsicCameraParams2
函数,该函数opencv/solvepnp.cpp at 8f15a609afc3c08ea0a5561ca26f1cf182414ca2 · opencv/opencv定义如下:
/* Finds extrinsic camera parameters from
a few known corresponding point pairs and intrinsic parameters */
void cvFindExtrinsicCameraParams2( const CvMat* object_points,
const CvMat* image_points,
const CvMat* camera_matrix,
const CvMat* distortion_coeffs,
CvMat* rotation_vector,
CvMat* translation_vector,
int use_extrinsic_guess CV_DEFAULT(0) );
其实现的cvFindExtrinsicCameraParams2
函数位于opencv/calibration.cpp at 8f15a609afc3c08ea0a5561ca26f1cf182414ca2 · opencv/opencv如下:
CV_IMPL void cvFindExtrinsicCameraParams2( const CvMat* objectPoints,
const CvMat* imagePoints, const CvMat* A,
const CvMat* distCoeffs, CvMat* rvec, CvMat* tvec,
int useExtrinsicGuess )
{
const int max_iter = 20;
Ptr<CvMat> matM, _Mxy, _m, _mn, matL;
int i, count;
double a[9], ar[9]={1,0,0,0,1,0,0,0,1}, R[9];
double MM[9], U[9], V[9], W[3];
cv::Scalar Mc;
double param[6];
CvMat matA = cvMat( 3, 3, CV_64F, a );
CvMat _Ar = cvMat( 3, 3, CV_64F, ar );
CvMat matR = cvMat( 3, 3, CV_64F, R );
CvMat _r = cvMat( 3, 1, CV_64F, param );
CvMat _t = cvMat( 3, 1, CV_64F, param + 3 );
CvMat _Mc = cvMat( 1, 3, CV_64F, Mc.val );
CvMat _MM = cvMat( 3, 3, CV_64F, MM );
CvMat matU = cvMat( 3, 3, CV_64F, U );
CvMat matV = cvMat( 3, 3, CV_64F, V );
CvMat matW = cvMat( 3, 1, CV_64F, W );
CvMat _param = cvMat( 6, 1, CV_64F, param );
CvMat _dpdr, _dpdt;
CV_Assert( CV_IS_MAT(objectPoints) && CV_IS_MAT(imagePoints) &&
CV_IS_MAT(A) && CV_IS_MAT(rvec) && CV_IS_MAT(tvec) );
count = MAX(objectPoints->cols, objectPoints->rows);
matM.reset(cvCreateMat( 1, count, CV_64FC3 ));
_m.reset(cvCreateMat( 1, count, CV_64FC2 ));
cvConvertPointsHomogeneous( objectPoints, matM );
cvConvertPointsHomogeneous( imagePoints, _m );
cvConvert( A, &matA );
CV_Assert( (CV_MAT_DEPTH(rvec->type) == CV_64F || CV_MAT_DEPTH(rvec->type) == CV_32F) &&
(rvec->rows == 1 || rvec->cols == 1) && rvec->rows*rvec->cols*CV_MAT_CN(rvec->type) == 3 );
CV_Assert( (CV_MAT_DEPTH(tvec->type) == CV_64F || CV_MAT_DEPTH(tvec->type) == CV_32F) &&
(tvec->rows == 1 || tvec->cols == 1) && tvec->rows*tvec->cols*CV_MAT_CN(tvec->type) == 3 );
CV_Assert((count >= 4) || (count == 3 && useExtrinsicGuess)); // it is unsafe to call LM optimisation without an extrinsic guess in the case of 3 points. This is because there is no guarantee that it will converge on the correct solution.
_mn.reset(cvCreateMat( 1, count, CV_64FC2 ));
_Mxy.reset(cvCreateMat( 1, count, CV_64FC2 ));
// normalize image points
// (unapply the intrinsic matrix transformation and distortion)
cvUndistortPoints( _m, _mn, &matA, distCoeffs, 0, &_Ar );
if( useExtrinsicGuess )
{
CvMat _r_temp = cvMat(rvec->rows, rvec->cols,
CV_MAKETYPE(CV_64F,CV_MAT_CN(rvec->type)), param );
CvMat _t_temp = cvMat(tvec->rows, tvec->cols,
CV_MAKETYPE(CV_64F,CV_MAT_CN(tvec->type)), param + 3);
cvConvert( rvec, &_r_temp );
cvConvert( tvec, &_t_temp );
}
else
{
Mc = cvAvg(matM);
cvReshape( matM, matM, 1, count );
cvMulTransposed( matM, &_MM, 1, &_Mc );
cvSVD( &_MM, &matW, 0, &matV, CV_SVD_MODIFY_A + CV_SVD_V_T );
// initialize extrinsic parameters
if( W[2]/W[1] < 1e-3)
{
// a planar structure case (all M's lie in the same plane)
double tt[3], h[9], h1_norm, h2_norm;
CvMat* R_transform = &matV;
CvMat T_transform = cvMat( 3, 1, CV_64F, tt );
CvMat matH = cvMat( 3, 3, CV_64F, h );
CvMat _h1, _h2, _h3;
if( V[2]*V[2] + V[5]*V[5] < 1e-10 )
cvSetIdentity( R_transform );
if( cvDet(R_transform) < 0 )
cvScale( R_transform, R_transform, -1 );
cvGEMM( R_transform, &_Mc, -1, 0, 0, &T_transform, CV_GEMM_B_T );
for( i = 0; i < count; i++ )
{
const double* Rp = R_transform->data.db;
const double* Tp = T_transform.data.db;
const double* src = matM->data.db + i*3;
double* dst = _Mxy->data.db + i*2;
dst[0] = Rp[0]*src[0] + Rp[1]*src[1] + Rp[2]*src[2] + Tp[0];
dst[1] = Rp[3]*src[0] + Rp[4]*src[1] + Rp[5]*src[2] + Tp[1];
}
cvFindHomography( _Mxy, _mn, &matH );
if( cvCheckArr(&matH, CV_CHECK_QUIET) )
{
cvGetCol( &matH, &_h1, 0 );
_h2 = _h1; _h2.data.db++;
_h3 = _h2; _h3.data.db++;
h1_norm = std::sqrt(h[0]*h[0] + h[3]*h[3] + h[6]*h[6]);
h2_norm = std::sqrt(h[1]*h[1] + h[4]*h[4] + h[7]*h[7]);
cvScale( &_h1, &_h1, 1./MAX(h1_norm, DBL_EPSILON) );
cvScale( &_h2, &_h2, 1./MAX(h2_norm, DBL_EPSILON) );
cvScale( &_h3, &_t, 2./MAX(h1_norm + h2_norm, DBL_EPSILON));
cvCrossProduct( &_h1, &_h2, &_h3 );
cvRodrigues2( &matH, &_r );
cvRodrigues2( &_r, &matH );
cvMatMulAdd( &matH, &T_transform, &_t, &_t );
cvMatMul( &matH, R_transform, &matR );
}
else
{
cvSetIdentity( &matR );
cvZero( &_t );
}
cvRodrigues2( &matR, &_r );
}
else
{
// non-planar structure. Use DLT method
double* L;
double LL[12*12], LW[12], LV[12*12], sc;
CvMat _LL = cvMat( 12, 12, CV_64F, LL );
CvMat _LW = cvMat( 12, 1, CV_64F, LW );
CvMat _LV = cvMat( 12, 12, CV_64F, LV );
CvMat _RRt, _RR, _tt;
CvPoint3D64f* M = (CvPoint3D64f*)matM->data.db;
CvPoint2D64f* mn = (CvPoint2D64f*)_mn->data.db;
matL.reset(cvCreateMat( 2*count, 12, CV_64F ));
L = matL->data.db;
for( i = 0; i < count; i++, L += 24 )
{
double x = -mn[i].x, y = -mn[i].y;
L[0] = L[16] = M[i].x;
L[1] = L[17] = M[i].y;
L[2] = L[18] = M[i].z;
L[3] = L[19] = 1.;
L[4] = L[5] = L[6] = L[7] = 0.;
L[12] = L[13] = L[14] = L[15] = 0.;
L[8] = x*M[i].x;
L[9] = x*M[i].y;
L[10] = x*M[i].z;
L[11] = x;
L[20] = y*M[i].x;
L[21] = y*M[i].y;
L[22] = y*M[i].z;
L[23] = y;
}
cvMulTransposed( matL, &_LL, 1 );
cvSVD( &_LL, &_LW, 0, &_LV, CV_SVD_MODIFY_A + CV_SVD_V_T );
_RRt = cvMat( 3, 4, CV_64F, LV + 11*12 );
cvGetCols( &_RRt, &_RR, 0, 3 );
cvGetCol( &_RRt, &_tt, 3 );
if( cvDet(&_RR) < 0 )
cvScale( &_RRt, &_RRt, -1 );
sc = cvNorm(&_RR);
CV_Assert(fabs(sc) > DBL_EPSILON);
cvSVD( &_RR, &matW, &matU, &matV, CV_SVD_MODIFY_A + CV_SVD_U_T + CV_SVD_V_T );
cvGEMM( &matU, &matV, 1, 0, 0, &matR, CV_GEMM_A_T );
cvScale( &_tt, &_t, cvNorm(&matR)/sc );
cvRodrigues2( &matR, &_r );
}
}
cvReshape( matM, matM, 3, 1 );
cvReshape( _mn, _mn, 2, 1 );
// refine extrinsic parameters using iterative algorithm
CvLevMarq solver( 6, count*2, cvTermCriteria(CV_TERMCRIT_EPS+CV_TERMCRIT_ITER,max_iter,FLT_EPSILON), true);
cvCopy( &_param, solver.param );
for(;;)
{
CvMat *matJ = 0, *_err = 0;
const CvMat *__param = 0;
bool proceed = solver.update( __param, matJ, _err );
cvCopy( __param, &_param );
if( !proceed || !_err )
break;
cvReshape( _err, _err, 2, 1 );
if( matJ )
{
cvGetCols( matJ, &_dpdr, 0, 3 );
cvGetCols( matJ, &_dpdt, 3, 6 );
cvProjectPoints2( matM, &_r, &_t, &matA, distCoeffs,
_err, &_dpdr, &_dpdt, 0, 0, 0 );
}
else
{
cvProjectPoints2( matM, &_r, &_t, &matA, distCoeffs,
_err, 0, 0, 0, 0, 0 );
}
cvSub(_err, _m, _err);
cvReshape( _err, _err, 1, 2*count );
}
cvCopy( solver.param, &_param );
_r = cvMat( rvec->rows, rvec->cols,
CV_MAKETYPE(CV_64F,CV_MAT_CN(rvec->type)), param );
_t = cvMat( tvec->rows, tvec->cols,
CV_MAKETYPE(CV_64F,CV_MAT_CN(tvec->type)), param + 3 );
cvConvert( &_r, rvec );
cvConvert( &_t, tvec );
}
该函数代码中最关键的是函数solver
,因为有一个参数max_iter
被传入:
// refine extrinsic parameters using iterative algorithm
CvLevMarq solver( 6, count*2, cvTermCriteria(CV_TERMCRIT_EPS+CV_TERMCRIT_ITER,max_iter,FLT_EPSILON), true);
cvCopy( &_param, solver.param );
其定义如下:
CvLevMarq
类的定义,位于opencv/calib3d_c.h at 8f15a609afc3c08ea0a5561ca26f1cf182414ca2 · opencv/opencv,其实现的构造、初始化、析构、参数更新等成员函数位于opencv/compat_ptsetreg.cpp at 8f15a609afc3c08ea0a5561ca26f1cf182414ca2 · opencv/opencv。
类CvLevMarq
定义:
class CV_EXPORTS CvLevMarq
{
public:
CvLevMarq();
CvLevMarq( int nparams, int nerrs, CvTermCriteria criteria=
cvTermCriteria(CV_TERMCRIT_EPS+CV_TERMCRIT_ITER,30,DBL_EPSILON),
bool completeSymmFlag=false );
~CvLevMarq();
void init( int nparams, int nerrs, CvTermCriteria criteria=
cvTermCriteria(CV_TERMCRIT_EPS+CV_TERMCRIT_ITER,30,DBL_EPSILON),
bool completeSymmFlag=false );
bool update( const CvMat*& param, CvMat*& J, CvMat*& err );
bool updateAlt( const CvMat*& param, CvMat*& JtJ, CvMat*& JtErr, double*& errNorm );
void clear();
void step();
enum { DONE=0, STARTED=1, CALC_J=2, CHECK_ERR=3 };
cv::Ptr<CvMat> mask;
cv::Ptr<CvMat> prevParam;
cv::Ptr<CvMat> param;
cv::Ptr<CvMat> J;
cv::Ptr<CvMat> err;
cv::Ptr<CvMat> JtJ;
cv::Ptr<CvMat> JtJN;
cv::Ptr<CvMat> JtErr;
cv::Ptr<CvMat> JtJV;
cv::Ptr<CvMat> JtJW;
double prevErrNorm, errNorm;
int lambdaLg10;
CvTermCriteria criteria;
int state;
int iters;
bool completeSymmFlag;
int solveMethod;
};
在cvFindExtrinsicCameraParams2
函数的实现中,初始化了CvLevMarq solver
,在后面该实例会更新update
,其实现如下:
bool CvLevMarq::update( const CvMat*& _param, CvMat*& matJ, CvMat*& _err )
{
matJ = _err = 0;
assert( !err.empty() );
if( state == DONE )
{
_param = param;
return false;
}
if( state == STARTED )
{
_param = param;
cvZero( J );
cvZero( err );
matJ = J;
_err = err;
state = CALC_J;
return true;
}
if( state == CALC_J )
{
cvMulTransposed( J, JtJ, 1 );
cvGEMM( J, err, 1, 0, 0, JtErr, CV_GEMM_A_T );
cvCopy( param, prevParam );
step();
if( iters == 0 )
prevErrNorm = cvNorm(err, 0, CV_L2);
_param = param;
cvZero( err );
_err = err;
state = CHECK_ERR;
return true;
}
assert( state == CHECK_ERR );
errNorm = cvNorm( err, 0, CV_L2 );
if( errNorm > prevErrNorm )
{
if( ++lambdaLg10 <= 16 )
{
step();
_param = param;
cvZero( err );
_err = err;
state = CHECK_ERR;
return true;
}
}
lambdaLg10 = MAX(lambdaLg10-1, -16);
if( ++iters >= criteria.max_iter ||
cvNorm(param, prevParam, CV_RELATIVE_L2) < criteria.epsilon )
{
_param = param;
state = DONE;
return true;
}
prevErrNorm = errNorm;
_param = param;
cvZero(J);
matJ = J;
_err = err;
state = CALC_J;
return true;
}
有没有定义 抬头低头的具体pitch值啊?
1. 什么是物体姿态和PNP问题
在计算机视觉中,物体的姿势指的是其相对于相机的相对取向和位置,一般用旋转矩阵、旋转向量、四元数或欧拉角表示(这四个量也可以互相转换)。一般而言,欧拉角可读性更好一些,也可以可视化(见下图,分别对应欧拉角的三个角度),所以常用欧拉角表示。欧拉角包含3个角度:pitch、yaw、roll,这三个角度也称为姿态角。
确定物体的姿态就是要计算其相对相机的欧拉角,刚提到也可以是旋转矩阵。旋转矩阵可以和欧拉角相互转换,一般而言,得到的欧拉角可以可视化到二维图像上,见下图。
姿势估计问题通常称为Perspective-n-Point问题或计算机视觉术语中的PNP。在该问题中,目标是在我们有校准相机时找到物体的姿势或运动(即找到欧拉角或旋转矩阵),还需要知道物体上的n个3D点的位置以及相应的相机3D到图像2D平面的投影。即,我们需要三个东西(后文会说到为什么需要这三样):
2. 如何在数学上表示物体相对相机的运动?
在此前,我们先看看怎么表示物体在3D世界坐标中的运动(即如何让3D世界坐标下的物体运动到相机3D世界中去)。3D刚性物体相对于相机的运动,仅具有两种运动:
(X,Y,Z)
移动到新的3D位置(X',Y',Z')
称为平移,平移具有3个自由度,即可以沿X,Y或Z方向移动。平移由向量t
表示(X' - X,Y' - Y,Z' - Z)
。3x3
维度的旋转矩阵或者3x1
维度的旋转向量,或者用一个旋转方向(即轴)和角度来定义。因此,估计3D对象中某一个点的运动,意味着找到该点的平移与旋转的6个参数 (这里,假定旋转用
1x3
维度的旋转向量表示):3个用于平移,3个用于旋转。那么我们就描述完了如何将3D世界中的坐标点转换到3D相机世界中去,这是第一个转换。但需要注意的是,我们需要知道物体3D世界中的点,而且一个假定是面部特征的3d坐标是基于世界坐标。这部分内容就是将物体在3D世界中的点,平移旋转到3D相机世界坐标的方法,但我只知道相机参数,并不知道物体在3D相机世界中的坐标。
3. 姿势估计算法如何工作?
别急,你看上图里有三个坐标系(世界3D坐标World Coordinates、相机3D坐标Camera Coordinates、投影平面坐标即图像Image Plane)。前文中有一张图片显示就是面部特征在世界3D坐标中的样子,以及对应2D图像Image Plane上的样子。如果我们知道,从人脸在世界3D坐标中的位置,到人脸在相机3D世界的位置的旋转和平移(即姿势/物体运动)参数,那么就可知道人脸在相机3D坐标中的位置。
仍旧看上图,可以发现相机3D坐标Camera Coordinates与投影平面坐标即图像Image Plane,二者的X和Y是一样的。现在我们知道世界坐标中位置P的3D点为(U,V,W)。如果知道相对于相机坐标的世界坐标的旋转矩阵R(3×3矩阵)和平移b(3×1矢量),就可以使用以下等式计算相机坐标系(X,Y,Z)中点的位置P。
也可以写成扩展的形式:
如果我们知道足够数量的点对,即(X,Y,Z)和与之对应的(U,V,W),上面就是一个线性方程组,可以求解得到旋转R和平移b的值。但该方法也有缺点(具体见下文方法2),正如前面所说,描述物体运动需要旋转和平移,对应3个方向6个值,这个方法一个3D点的平移参数算出是3个值,但是旋转参数却有9个(假定我们这里不考虑使用
3x3
维度的旋转矩阵,而是想要使用3x1
维度的旋转向量)。3.1 方法1:直接线性变换(DLT)求解参数
在上面的方程中,我们有很多已知的3D世界坐标点,即(U,V,W),但不知道具体的3D相机坐标(X,Y,Z),但知道3D相机坐标中的X和Y,即2D图像上的关键点。在没有径向畸变(即z点的畸变)的情况下,2D图像坐标中P点即(x,y)有以下公式:
PS:下面公式描述了,将相机坐标(X,Y,Z)转换即投影为平面坐标(x,y,1)的过程,已知平面坐标和相机参数,通过下面公式算出相机坐标(X,Y,Z):
其中,
f_x
和f_y
是在x和y方向上的焦距,并且(c_x,c_y)
是光学中心。当涉及径向变形时,事情变得稍微复杂一些,为了简单起见不考虑这一项。s是一个我们未知的比例因子,表示深度信息。举个例子,若将3D世界坐标中某个点P与3D相机坐标中对应的点P连接起来,即光线,那么这个光线会与图像平面相交得到二维平面上的点P。这个s就是这个过程中变换的深度信息,但我们这里因为未知,统统将所有点s考虑为一样的。
PS:以下公式,将3D世界坐标(U,V,W)转换为3D相机坐标,已知3D世界坐标和上面公式算出的3D相机坐标,因而可以算出
3x3
的旋转矩阵和3x1
的平移向量。基于上面的两个公式,先用第一个公式计算出Z值,再用第二个公式计算出旋转矩阵,依次列出方程组,依次求解得到3D相机坐标的值Z、旋转R和平移T参数。这种方法称为直接线性变换(Direct Linear Transform,DLT)。然后再讲旋转矩阵转换为欧拉角。
下面是补充内容,将
3x3
维度的旋转矩阵转换为欧拉角。3.2 方法2:Levenberg-Marquardt
实际中,方法1的直接线性变换(Direct Linear Transform,DLT)计算结果不准确,因为:
3x3
矩阵的意义是旋转矩阵(这里我有点懵,可能是说,3x3
维度的这个矩阵其实际意义很难说带有旋转的意思在里面,因为没有最小化目标函数,这个算出的3x3
的矩阵与旋转的参数意义不明确);前文中的公式中,我们已经知道,若得到了正确的姿态R和t参数,就可以基于3D点信息以3D点投射到2D图像上的方式,来得到2D点信息。换句话说,知道了姿态参数R和t就可以知道每个3D点投影的2D点。
实际计算姿态前,我们已知2D点位置信息(比方用Dlib或手动选择点击得到关键点),假设也已知旋转R和平移t参数,那么基于2D点、R和t,我们可以算出2D点对应投影出的3D点(这个3D点是我们用参数R和t估算的),因为我们也知道真实的3D点,进而可以得到估算的3D点与真实的3D点二者的投影误差(re-projection error),即所有相对应点的距离平方和。
用前文中提到的两个公式(如上,再次列出),直接线性变换(DLT,direct linear transform)就是对姿态参数R和t的估计,想要改进一个最简单的方法就是修正R和t参数,看重投影误差有没有降低。如果降低,则接受新的R和t参数。但这个方法没有修正目标(每次加一点轻微扰动),而且很慢,一种名为LM的优化方法(Levenberg-Marquardt optimization),可以很好地解决这个问题,具体参考维基百科。
OpenCV的API
solvePnP
(PnP
,Perspective-n-Point,即从3D点找到其对应的2D点),可以使用基于LM方法(Levenberg-Marquardt)进行姿态估计,只需要定义flag参数为CV_ITERATIVE
即可(当然还有其他方法,见后文API说明),无论哪种方法的使用,都需要定义这三个参数:objectPoints
;imagePoints。如人脸关键点位置信息(五点如左右眼睛、嘴唇左右、鼻子)
;distCoeffs
(一般是固定的,其中定义了焦距,光学中心等)。确保或者以该参数校准相机,不会有误差、错误的结果等。有了上面三个参数交给LM优化算法,它就会使用相机的固有参数,结合世界坐标中的3D点与图像平面上的点信息,估计姿态的旋转R和平移t参数(即图像坐标系),反复迭代计算并寻找重投影误差最小时的旋转和平移参数,即我们最后期望的结果。
实际上,OpenCV有两个用来解决计算物体姿态函数接口:
solvePnP
、solvePnPRansac
,第二个solvePnPRansac
利用随机抽样一致算法(Random sample consensus,RANSAC)的思想,虽然计算出的姿态更加精确,但速度慢、没法实现实时系统,所以我们这里只关注第一个solvePnP
函数,OpenCV提供其C++与Python接口,具体说明如下:4. Levenberg-Marquardt优化算法
【见后文】【见后文】【见后文】【见后文】【见后文】【见后文】 【】【】【】【】【】【】【】【】【】【】【】【】【】
5. 旋转参数转换为欧拉角
副标题:旋转向量(角轴)、四元数与欧拉角的转换
前面我们使用OpenCV的
solvePnP
接口,得到了旋转向量R和平移向量t。现在我们需要将旋转向量R转换为欧拉角。后面有与之对应实现的具体代码。这个代码的过程包含以下两个转换部分:rotation_vector
——> 四元数(quaterniond,即w,x,y,z
);w,x,y,z
)——>对应的欧拉角(pitch、yaw、roll)。注:更多旋转方面的转换公式见后面参考链接。
5.1 旋转向量(角轴)转为四元数
假设某个旋转是绕单位向量
n = [n_x, n_y, n_z]^T
进行角度为\theta
的旋转,则有四元数q
:q = [cos(\theta / 2), n_x sin(\theta / 2), n_y sin(\theta / 2), n_z sin(\theta / 2)] ^ T
,q = [w, x, y, z]^T
,|q|^2 = w^2 + x^2 + y^2 + z^2 = 1
该转换公式中的
\theta
加上2pi
,得到一个相同的旋转,其对应的四元数变成了-q
,即任意的旋转都可以由两个互为相反数的四元数表示。对应如下代码:
5.2 四元数转为欧拉角
得到四元数后,再计算欧拉角,公式如下:
由于
arctan
和arcsin
的取值范围在[−π/2, π/2]
,之间只有180°
,而绕某个轴旋转时范围是360°
,因此要使用atan2
函数代替arctan
函数:从四元数
[w, x, y, z]
转换为欧拉角的代码实现:计算得到的欧拉角是弧度,需要将弧度转换为角度,才是我们姿态估计时候的欧拉角,转换公式如下:
6. 欧拉角与头部姿态
知道了欧拉角后,要确定头部姿态需要计算不同姿态对应的三个角度(pitch、yaw、roll)的值范围。常用的头部姿态有如下五个:左右摇头(水平45°倾斜,或者说是绕x轴旋转),左右转头(yaw,绕z轴旋转),抬头点头(一般看点头,不看抬头因为抬头和人脸正面差不多)。
这个参数需要调并校准,我在网上找了很多人脸图,然后对其旋转,得到3组图片:
分别先跑网络得到关键点,观察计算出的角度,确认每张图片的角度范围。得出:
这个对应姿态的三个角度参数需要自己调整,从而判断当前的头部属于哪种姿态。