Notifications
Article
Unity SenseAR教程:人脸追踪2之探索挂点位置【含源码】
Published 4 years ago
602
0
探索SenseAR中人脸追踪眼睛、鼻子、嘴巴挂点的位置,还送你一个开箱即用的扩展工具类哦~
洪流学堂,让你快人几步。你好,我是你的技术探路者郑洪智,你可以叫我大智(VX: zhz11235)。
上次咱们一起探索了人脸追踪,并且实现了通过点击往脸上“添彩”的功能。但是很多时候,咱们想识别出脸部后直接在脸上给它添加一些装饰物,而不需要玩家手动点击脸上才能添加上。
上一篇最后也给你了一些思路,就是根据射线检测到的点,计算出离点击点最近的顶点,这个顶点顺序大概率是不会变的,可以作为锚定的坐标点。咱们这节课一起使用这个思路来探索一下是否可行。
对SenseAR还不太熟悉的同学可以看下大智的视频:
  • 商汤SenseAR全功能初体验(含填坑经验)
  • 视频B站链接:https://www.bilibili.com/video/av89332645

最终效果

首先要给你颗定心丸,上面的思路是可行的。这次不需要手动点击往脸上放小球了,小球可以直接出现!

开工

想要达成今天的目标,咱们需要依次解决以下几个问题:
1、首先确认脸部Mesh的顶点数是固定的(否则顶点索引可能会变化很大)
2、使用上节的射线检测到的点,计算脸部Mesh上离这个点最近的点的索引
3、记录下几个点的索引位置,在对应位置生成小球,验证下是否每次都是固定点
4、编写一个ARFace扩展类,可以直接获取对应位置的点

1、确认脸部Mesh的定点数

首先确认脸部Mesh的顶点数是固定的,否则顶点索引可能会变化很大
这个数字可以在ARFace.vertices.Length获取到
void Update() { if (m_FaceManager.subsystem != null && faceInfoText != null) { faceInfoText.text = $"Supported number of tracked faces: {m_FaceManager.supportedFaceCount}\n" + $"Max number of faces to track: {m_FaceManager.maximumFaceCount}\n" + $"Number of tracked faces: {m_FaceManager.trackables.count}"; // 这样可以在UI上看到顶点的数量 faceInfoText.text += "\n当前脸部Mesh的顶点数为:" + _verticeCount; } if (Input.GetMouseButtonUp(0)) { var camera = GetComponent<ARSessionOrigin>().camera; var ray = camera.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out var hit, 1000)) { var go = GameObject.CreatePrimitive(PrimitiveType.Sphere); go.transform.localScale = Vector3.one * 0.01f; // 设置父物体为人脸,这样物体会跟随人脸移动 go.transform.SetParent(hit.transform); go.transform.position = hit.point; // !!!下面是添加的代码 var face = hit.transform.GetComponent<ARFace>(); // 创建一个int类型的私有成员 _verticeCount = face.vertices.Length; } } }
通过这一步,咱们就能确认脸部的网格固定是11510个顶点了,可以放心进入第二步了。

2、计算脸部Mesh上离射线检测点最近的顶点的索引

这一步咱们需要找到几个特殊点的索引,我准备找到的点是鼻尖、4个眼角、2个嘴角。
代码如下:
void Update() { if (m_FaceManager.subsystem != null && faceInfoText != null) { faceInfoText.text = $"Supported number of tracked faces: {m_FaceManager.supportedFaceCount}\n" + $"Max number of faces to track: {m_FaceManager.maximumFaceCount}\n" + $"Number of tracked faces: {m_FaceManager.trackables.count}"; // 这样可以在UI上看到顶点的数量 faceInfoText.text += "\n当前脸部Mesh的顶点数为:" + _verticeCount; // 这样可以在UI上看到顶点的索引 faceInfoText.text += "\n离点击位置最近的顶点索引是:" + _verticeCount; } // !!!下面是添加的代码 if (Input.GetMouseButtonUp(0)) { var camera = GetComponent<ARSessionOrigin>().camera; var ray = camera.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out var hit, 1000)) { var go = GameObject.CreatePrimitive(PrimitiveType.Sphere); go.transform.localScale = Vector3.one * 0.01f; // 设置父物体为人脸,这样物体会跟随人脸移动 go.transform.SetParent(hit.transform); go.transform.position = hit.point; var face = hit.transform.GetComponent<ARFace>(); // 需要在类中创建一个int类型的私有成员 _verticeCount = face.vertices.Length; var min = float.MaxValue; // 需要在类中创建一个int类型的私有成员 minIndex = -1; for (var index = 0; index < face.vertices.Length; index++) { var v = face.vertices[index]; var local = hit.transform.InverseTransformPoint(hit.point); // 使用sqrMagnitude可以减少一次开方计算,结果一样,性能更好 var distance = (v - local).sqrMagnitude; if (distance < min) { minIndex = index; min = distance; } } } } }
我找到的几个点索引是:
private int[] PointIndexs = {10655, 9265, 9218, 10796, 8940, 10609, 9103};

3、反向验证第2步得到的索引

记录下几个点的索引位置,在对应位置生成小球,验证下是否每次都是固定点
using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.XR.ARFoundation; [RequireComponent(typeof(ARFaceManager))] public class DisplayFaceInfo : MonoBehaviour { [SerializeField] Text m_FaceInfoText; public Text faceInfoText { get { return m_FaceInfoText; } set { m_FaceInfoText = value; } } ARFaceManager m_FaceManager; private int minIndex; private int[] PointIndexs = {10655, 9265, 9218, 10796, 8940, 10609, 9103}; // 下面的颜色是调试用的,因为大智忘了上面那些数字对应是那些位置了 /(ㄒoㄒ)/~~ private Color[] Colors = {Color.black, Color.white, Color.blue, Color.gray, Color.green, Color.red, Color.yellow}; private Dictionary<int, GameObject> BallMap = new Dictionary<int, GameObject>(); private int _verticeCount; void Awake() { m_FaceManager = GetComponent<ARFaceManager>(); m_FaceManager.facesChanged += delegate(ARFacesChangedEventArgs args) { if (args.added.Count > 0) { var face = args.added[0]; for (var i = 0; i < PointIndexs.Length; i++) { var index = PointIndexs[i]; var go = GameObject.CreatePrimitive(PrimitiveType.Sphere); go.transform.localScale = Vector3.one * 0.01f; var pos = face.vertices[index]; // 设置父物体为人脸,这样物体会跟随人脸移动 go.transform.SetParent(face.transform); go.transform.localPosition = pos; go.GetComponent<Renderer>().material.color = Colors[]; BallMap[index] = go; } } // 更新点的位置,added的时候可能mesh还不准确,顶点位置有可能更新 if (args.updated.Count > 0) { var face = args.updated[0]; foreach (var index in PointIndexs) { var pos = face.vertices[index]; var go = BallMap[index]; go.transform.localPosition = pos; } } }; } void Update() { if (m_FaceManager.subsystem != null && faceInfoText != null) { faceInfoText.text = $"Supported number of tracked faces: {m_FaceManager.supportedFaceCount}\n" + $"Max number of faces to track: {m_FaceManager.maximumFaceCount}\n" + $"Number of tracked faces: {m_FaceManager.trackables.count}"; // 这样可以在UI上看到顶点的数量 faceInfoText.text += "\n当前脸部Mesh的顶点数为:" + _verticeCount; // 这样可以在UI上看到顶点的索引 faceInfoText.text += "\n离点击位置最近的顶点索引是:" + _verticeCount; } // !!!下面是添加的代码 if (Input.GetMouseButtonUp(0)) { var camera = GetComponent<ARSessionOrigin>().camera; var ray = camera.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out var hit, 1000)) { var go = GameObject.CreatePrimitive(PrimitiveType.Sphere); go.transform.localScale = Vector3.one * 0.01f; // 设置父物体为人脸,这样物体会跟随人脸移动 go.transform.SetParent(hit.transform); go.transform.position = hit.point; var face = hit.transform.GetComponent<ARFace>(); // 需要在类中创建一个int类型的私有成员 _verticeCount = face.vertices.Length; var min = float.MaxValue; // 需要在类中创建一个int类型的私有成员 minIndex = -1; for (var index = 0; index < face.vertices.Length; index++) { var v = face.vertices[index]; var local = hit.transform.InverseTransformPoint(hit.point); // 使用sqrMagnitude可以减少一次开方计算,结果一样,性能更好 var distance = (v - local).sqrMagnitude; if (distance < min) { minIndex = index; min = distance; } } } } } }
通过执行上面的代码试验几次(最好在不同的人脸上测试下),你会发现这些顶点索引是固定的,并不会变化,咱们以后就可以根据这些点的索引来获取对应位置。

写一个工具类

人脸对应下面枚举的点如下图(以下位置是真人脸上的位置,注意前置相机是左右镜像状态):
需要注意一下这个脚本的位置:最好放到Example/Scripts下面。
不放在这个目录,Example/Scripts目录下的脚本中会找不到这个API。为什么呢?因为Example/Scripts中有一个ADF文件,相当于把这个目录的脚本单独设置成为了一个工程。
更多相关内容请阅读:程序集定义(Assembly Definition File)功能详解
// 首发公众号:洪流学堂 // 作者:大智(微信:zhz11235) using UnityEngine; using UnityEngine.XR.ARFoundation; // 以下位置是真人脸上的位置,注意前置相机是左右镜像状态 // 参考图:https://upload-images.jianshu.io/upload_images/78733-0653b6136bd7cd40.png public enum FaceAnchor { LeftEyeL = 10796, LeftEyeR = 10655, RightEyeL = 9218, RightEyeR = 9265, Nose = 8940, MouthL = 10609, MouthR = 9103, } public static class ARFaceExtensions { /// <summary> /// 根据锚点获基于脸部的局部坐标 /// </summary> /// <param name="face"></param> /// <param name="anchor"></param> /// <returns>face的局部坐标</returns> public static Vector3 GetAnchor(this ARFace face, FaceAnchor anchor) { int index = (int) anchor; if (face.vertices.Length > index) { return face.vertices[index]; } return Vector3.zero; } }
上面的点不一定是最准确的点,你可以根据这个思路来进行修改。还可以添加更多锚点的位置,比如额头、耳朵等。

扩展阅读

人脸追踪:射线检测添加装饰物
SenseAR的手势识别发射爱心
SenseAR的手势识别2:计算手势方向
商汤SenseAR全功能初体验(含填坑经验)
视频B站链接:https://www.bilibili.com/video/av89332645
SenseAR常见问题总结

本教程源码及后续更新

由于源码后续可能会更新,就不直接打包传在这里了。
本工程的持续更新源码可以在洪流学堂公众号回复face获取。
好了,今天就絮絮叨叨到这里了。
没讲清楚的地方欢迎评论,也可以加我微信讨论。
我是大智(VX: zhz11235),你的技术探路者,下次见!
别走!点赞,收藏哦!
好,你可以走了。

Tags: