unity怎么实现人脸追踪_OpenCVForUnity常见应用3——基于人脸的3D姿态估计

论坛 期权论坛 编程之家     
选择匿名的用户   2021-6-1 03:27   33   0

话不多说,首先贴上OpenCV的相关链接:

API文档:OpenCV: OpenCV modulesdocs.opencv.org

官方教程:OpenCV教程_w3cschoolwww.w3cschool.cn

就如标题所说,今天我们来解析一下OpenCVForUnity在AR应用当中的一个例子——人脸的3d姿态估计。这个示例也是插件作者写的,但是没有默认包括在基础插件中,需要额外下载一个FaceTracker包,Unity应用商店有(免费),如下图

导入后需要按照提供的文档进行一下配置,最终结果如下人脸的3d实时姿态估计,可以看出效果还是不错的

可以想象,这样的应用场景是非常多的,比如我们常见的FaceMask!

下面我们来详细分析它的原理!

打开ARHeadWebCamTextureExample.cs,先从

开始,当WebCam初始化完成后,这个方法被调用。它通过webCamTextureToMatHelper.GetMat ()获得相机图像。根据图像信息设置图像平面的大小,设置了正交相机的视口尺寸以适应图像网格的大小。这是每个例子的通用操作,不再赘述。

//获取相机图像

Mat webCamTextureMat = webCamTextureToMatHelper.GetMat ();

...

//设置展示图像的网格的尺寸

gameObject.transform.localScale = new Vector3 (webCamTextureMat.cols (), webCamTextureMat.rows (), 1);

//设置相机视口

float width = webCamTextureMat.width ();

float height = webCamTextureMat.height ();

float imageSizeScale = 1.0f;

float widthScale = (float)Screen.width / width;

float heightScale = (float)Screen.height / height;

if (widthScale < heightScale) {

Camera.main.orthographicSize = (width * (float)Screen.height / (float)Screen.width) / 2;

imageSizeScale = (float)Screen.height / (float)Screen.width;

} else {

Camera.main.orthographicSize = height / 2;

}

接着开始进入正题。开始前可以先看下官方关于OpenCV中进行姿态估计的教程:使用OpenCV相机校准_w3cschoolwww.w3cschool.cnOpenCV纹理对象的实时姿态估计_w3cschoolwww.w3cschool.cn

在进行姿态估计之前,需要先对相机进行校准,也在初始化中完成。

校准通过

//camMatrix:相机矩阵//imageSize:图像尺寸//apertureWidth:sensor宽度,单位mm//apertureHeight:sensor高度//fovx ,fovy :fov//focalLength:焦距,单位mm//principalPoint:主点(mm)//高宽比 fy/fxCalib3d.calibrationMatrixValues (camMatrix, imageSize, apertureWidth, apertureHeight, fovx, fovy, focalLength, principalPoint, aspectratio);

来完成,通过输入camMatrix,imageSize从先前估计的摄像机矩阵中计算出各种有用的摄像机特性。这些参数会在后面的姿态估计中使用。具体用法参看OpenCV的API文档。相机矩阵准备

其中cx,cy为图像中心点(像素坐标表示的光学中心),fx,fy表示摄像机焦距(fx,fy通常相等,且等于比较大的那一个)。相机矩阵设置代码如下:

//set cameraparam int max_d = (int)Mathf.Max (width, height);

double fx = max_d;

double fy = max_d;

double cx = width / 2.0f;

double cy = height / 2.0f;

camMatrix = new Mat (3, 3, CvType.CV_64FC1);

camMatrix.put (0, 0, fx);

camMatrix.put (0, 1, 0);

camMatrix.put (0, 2, cx);

camMatrix.put (1, 0, 0);

camMatrix.put (1, 1, fy);

camMatrix.put (1, 2, cy);

camMatrix.put (2, 0, 0);

camMatrix.put (2, 1, 0);

camMatrix.put (2, 2, 1.0f);

Debug.Log ("camMatrix " + camMatrix.dump ());

//畸变系数设为0表示没有畸变 distCoeffs = new MatOfDouble (0, 0, 0, 0);Unity与OpenCV之间的转换

unity与opencv之间存在一些区别:OpenCV使用右手坐标系,Unity为左手坐标系;OpenCV中FOV与Unity中FOV也存在区别;相机坐标系中Z轴的前后关系等。所里在初始化方法中也对这些进行了一些转换。

FOV:

//To convert the difference of the FOV value of the OpenCV and Unity. double fovXScale = (2.0 * Mathf.Atan ((float) (imageSize.width / (2.0 * fx)))) / (Mathf.Atan2 ((float) cx, (float) fx) + Mathf.Atan2 ((float) (imageSize.width - cx), (float) fx));

double fovYScale = (2.0 * Mathf.Atan ((float) (imageSize.height / (2.0 * fy)))) / (Mathf.Atan2 ((float) cy, (float) fy) + Mathf.Atan2 ((float) (imageSize.height - cy), (float) fy));

Debug.Log ("fovXScale " + fovXScale);

Debug.Log ("fovYScale " + fovYScale);

//Adjust Unity Camera FOV https://github.com/opencv/opencv/commit/8ed1945ccd52501f5ab22bdec6aa1f91f1e2cfd4 if (widthScale < heightScale) {

ARCamera.fieldOfView = (float) (fovx[0] * fovXScale);

} else {

ARCamera.fieldOfView = (float) (fovy[0] * fovYScale);

}

左右手坐标系转换矩阵

invertYM = Matrix4x4.TRS (Vector3.zero, Quaternion.identity, new Vector3 (1, -1, 1));

Z轴向转换矩阵

invertZM = Matrix4x4.TRS (Vector3.zero, Quaternion.identity, new Vector3 (1, 1, -1));

姿态估计

完整的姿态估计在Update方法中。为了尽量提高运行效率,首先获取人脸的矩形区域detectResult,这样后面进行人脸特征点检测就只需要在这一小片区域进行。最终获得特征点List points。

//给faceLandmarkDetector提供图像,faceLandmarkDetector为人脸特征点检测类OpenCVForUnityUtils.SetImage (faceLandmarkDetector, rgbaMat);

//获得人脸的矩形区域List detectResult = faceLandmarkDetector.Detect ();

......

//检测人脸特征点List points = faceLandmarkDetector.DetectLandmark (detectResult[0]);通过标定头模的3d空间位置objectPoints和对应的人脸特征点imagePoints,就可以通过solvePnP进行姿态估计了。最终的姿态数据就保存在rvec,tvec中。姿态估计示意图

头部姿态估计 //如果tvec是错误的数据或物体不在相机的视场中,则不对估计出的数据进行优化,降低计算量 if (double.IsNaN (tvec_z) || isNotInViewport) {

Calib3d.solvePnP (objectPoints, imagePoints, camMatrix, distCoeffs, rvec, tvec);

} else {

//objectPoints :世界空间中物体上的对象点数组 //imagePoints:与objectPoints对应的图像特征点 //camMatrix:相机矩阵 //distCoeffs:畸变参数(为0表示没有畸变) //rvec:输出旋转向量(见Rodrigues),它与tvec一起,将模型坐标系中的点引入摄像机坐标系 //tvec:输出和缩放向量 //useExtrinsicGuess:参数用于SOLVEPNP_ITERATIVE。若为真(1),函数分别将提供的rvec和tvec值作为旋转向量和平移向量的初始逼近,并对其进行进一步优化。 //flags:指定求解PnP问题的方法。 Calib3d.solvePnP (objectPoints, imagePoints, camMatrix, distCoeffs, rvec, tvec, true, Calib3d.SOLVEPNP_ITERATIVE);

}

对于objectPoints和imagePoints示例提供了多种特征点数量的版本(68,17,6,5点,数量越多,人脸特征信息越多,开销越大)。不同特征点数量的版本,每个特征点所表示的人脸位置也是不一样的,从下面的示意图就可以很明显的看出。68个特征点17个特征点

objectPoints与imagePoints的填充,它们存在一一对应的关系:

//set 3d face object points. objectPoints68 = new MatOfPoint3f (

new Point3 (-34, 90, 83), //l eye (Interpupillary breadth) new Point3 (34, 90, 83), //r eye (Interpupillary breadth) new Point3 (0.0, 50, 117), //nose (Tip) new Point3 (0.0, 32, 97), //nose (Subnasale) new Point3 (-79, 90, 10), //l ear (Bitragion breadth) new Point3 (79, 90, 10) //r ear (Bitragion breadth) );

//68特征点imagePoints.fromArray (

new Point ((points[38].x + points[41].x) / 2, (points[38].y + points[41].y) / 2), //l eye (Interpupillary breadth) new Point ((points[43].x + points[46].x) / 2, (points[43].y + points[46].y) / 2), //r eye (Interpupillary breadth) new Point (points[30].x, points[30].y), //nose (Tip) new Point (points[33].x, points[33].y), //nose (Subnasale) new Point (points[0].x, points[0].y), //l ear (Bitragion breadth) new Point (points[16].x, points[16].y) //r ear (Bitragion breadth) );注意到左图中箭头所指处的坐标z轴为-97,与new Point3 (0.0, 32, 97)相反,这是因为OpenCV中默认使用的右手坐标系不过为了去除不必要的计算,在进行姿态估计前,可以判断前一帧物体是否在相机视场内。

//剔除不在相机视野内的情况 double tvec_x = tvec.get (0, 0) [0], tvec_y = tvec.get (1, 0) [0], tvec_z = tvec.get (2, 0) [0];

bool isNotInViewport = false;

Vector4 pos = VP * new Vector4 ((float) tvec_x, (float) tvec_y, (float) tvec_z, 1.0f);

if (pos.w != 0) {

float x = pos.x / pos.w, y = pos.y / pos.w, z = pos.z / pos.w;

if (x < -1.0f || x > 1.0f || y < -1.0f || y > 1.0f || z < -1.0f || z > 1.0f)

isNotInViewport = true;

}

PV矩阵通过在初始化方法中通过下面方法计算得来:

// 计算AR相机的P*V矩阵,后面用来判断追踪物体是否超出相机范围// 下面方法可用此方法代替 Matrix4x4 P = ARUtils.CalculateProjectionMatrix (width, height, 0.3f, 2000f);Matrix4x4 P = ARUtils.CalculateProjectionMatrixFromCameraMatrixValues ((float) fx, (float) fy, (float) cx, (float) cy, width, height, 0.3f, 2000f);

Matrix4x4 V = Matrix4x4.TRS (Vector3.zero, Quaternion.identity, new Vector3 (1, 1, -1));

VP = P * V;

//CalculateProjectionMatrixFromCameraMatrixValues/// /// Calculate projection matrix from camera matrix values. /// /// Focal length x. /// Focal length y. /// Image center point x.(principal point x) /// Image center point y.(principal point y) /// Image width. /// Image height. /// The near clipping plane distance. /// The far clipping plane distance. /// /// Projection matrix. /// public static Matrix4x4 CalculateProjectionMatrixFromCameraMatrixValues(float fx, float fy, float cx, float cy, float width, float height, float near, float far)

{

Matrix4x4 projectionMatrix = new Matrix4x4();

projectionMatrix.m00 = 2.0f * fx / width;

projectionMatrix.m02 = 1.0f - 2.0f * cx / width;

projectionMatrix.m11 = 2.0f * fy / height;

projectionMatrix.m12 = -1.0f + 2.0f * cy / height;

projectionMatrix.m22 = -(far + near) / (far - near);

projectionMatrix.m23 = -2.0f * far * near / (far - near);

projectionMatrix.m32 = -1.0f;

return projectionMatrix;

}

至于这个投影矩阵的计算方法我也没搞太懂(后面搞懂了再来补充),不过这里我们也可以通过更简单的方式(通过Unity自带方法)来计算:

public static Matrix4x4 CalculateProjectionMatrix(float width,float height,float near,float far){

//https://docs.unity3d.com/Manual/FrustumSizeAtDistance.html float fov = 2 * Mathf.Atan2(height * 0.5f,far) * Mathf.Deg2Rad;

float aspect = width / height;

return Matrix4x4.Perspective(fov,aspect,near,far);

}

注意到前面计算P*V矩阵中的V矩阵时,只是简单的给V矩阵的Scale的Z轴填充-1(就是沿Z轴翻转),这是因为:从姿态估计运算中得到的tvec时基于OpenCV的从物体空间->相机坐标系的转换。而在OpenCV中使用右手坐标系,相机Z轴指向前方,这与Unity中有些区别(Unity中,相机空间使用右手坐标系,Z轴指向后方)。所以这里的V只需要将tvec沿Z轴翻转就行了。

转换

完成姿态估计得到rvec,tvec后,由于OpenCV空间和Unity空间的差别,还需要进行转换相关的操作。从rvec,tvec提取转换信息。首先通过ARUtils.ConvertRvecTvecToPoseData将rvec,tvec转换为Unity适用的poseData,然后通过简单低通滤波LowpassPoseData抑制小幅的抖动。最后将poseData转换为变换矩阵,方便下一步使用。

// Convert to unity pose data. double[] rvecArr = new double[3];

rvec.get (0, 0, rvecArr);

double[] tvecArr = new double[3];

tvec.get (0, 0, tvecArr);

//转换成适用于Unity的PoseData PoseData poseData = ARUtils.ConvertRvecTvecToPoseData (rvecArr, tvecArr);

//低通滤波,pos/rot中低于这些阈值的更改将被忽略。 if (enableLowPassFilter) {

ARUtils.LowpassPoseData (ref oldPoseData, ref poseData, positionLowPass, rotationLowPass);

}

oldPoseData = poseData;

//创建适用于Unity的变换矩阵 transformationM = Matrix4x4.TRS (poseData.pos, poseData.rot, Vector3.one);

转换到Unity适应的坐标系

//右手坐标系(OpenCV)到左手坐标系(Unity)ARM = invertYM * transformationM;

//翻转Z轴(OpenCV相机坐标系,z轴指向前面)ARM = ARM * invertZM;

最终的到了Unity中 物体坐标系->相机坐标系的变换矩阵ARM。

应用

最后就是应用变换了,例子里提供了两种方式:移动物体或者移动相机,来匹配图像与模型。一般情况下我们会选择移动相机(通常着更符合显示规律)。

//shouldMoveARCamera==true:移动相机,不移动物体 if (shouldMoveARCamera) {

//相机空间-》物体空间-》世界空间 ARM = ARGameObject.transform.localToWorldMatrix * ARM.inverse;

ARUtils.SetTransformFromMatrix (ARCamera.transform, ref ARM);

} else {

ARM = ARCamera.transform.localToWorldMatrix * ARM;

ARUtils.SetTransformFromMatrix (ARGameObject.transform, ref ARM);

}

至此整个基于人脸的3d姿态就完成了。

优化

如果感觉运行效率还是太低,可以参考FrameOptimizationExample中的方法:降低用于图像检测和估算的图像分辨率。

不要每帧都进行估算,可以选择隔几帧估算一次。

当然上面还有一些用到了的方法没有讲到,比如ARUtils中就有很多。这个就留给下一期吧。

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:3875789
帖子:775174
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP