-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Head pose estimation #9
Comments
7. 头部姿态估计实现代码具体代码说明,见前文内容。这部分是对前文内容基于LM方法的具体实现,LM方法调用OpenCV的 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 8. 头部姿态估计的评价准则 |
9. 其它优秀方法最新姿态估计研究进展,一般分为两种:
9.1 CMU:openpose 研究多人的姿态估计
9.2 Google DeepGaze:deepgaze 研究头部姿态和注意力方向。主要为头部姿态估计 先框出人脸区域,再进行姿态估计。
|
4. Levenberg-Marquardt优化算法注:本节的整体脉络来自这篇博文:Levenberg-Marquardt算法浅谈 | liu14lang,在此我加以补充和扩写。 讲Levenberg-Marquardt算法前,先有必要讲一下牛顿法(Newton's method )和高斯牛顿法(Quasi-Newton Methods)。
【】【】【】【】【】【】【】【】【】【】【】【】【】 Newton method · Issue #22 · yuenshome/yuenshome.github.io
|
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优化算法的实现是其中的 /* 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) ); 其实现的 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 );
} |
该函数代码中最关键的是函数 // 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 ); 其定义如下: |
类 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;
}; |
在 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组图片:
分别先跑网络得到关键点,观察计算出的角度,确认每张图片的角度范围。得出:
这个对应姿态的三个角度参数需要自己调整,从而判断当前的头部属于哪种姿态。
The text was updated successfully, but these errors were encountered: