Unity SenseAR教程:人脸追踪2之探索挂点位置【含源码】

摘要:探索SenseAR中人脸追踪眼睛、鼻子、嘴巴挂点的位置,还送你一个开箱即用的扩展工具类哦~

洪流学堂,让你快人几步。你好,我是你的技术探路者郑洪智,你可以叫我大智(VX: zhz11235)。

上次咱们一起探索了人脸追踪,并且实现了通过点击往脸上“添彩”的功能。但是很多时候,咱们想识别出脸部后直接在脸上给它添加一些装饰物,而不需要玩家手动点击脸上才能添加上。

上一篇最后也给你了一些思路,就是根据射线检测到的点,计算出离点击点最近的顶点,这个顶点顺序大概率是不会变的,可以作为锚定的坐标点。咱们这节课一起使用这个思路来探索一下是否可行。

对SenseAR还不太熟悉的同学可以看下大智的视频:

最终效果

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

开工

想要达成今天的目标,咱们需要依次解决以下几个问题:
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;
    }
}

上面的点不一定是最准确的点,你可以根据这个思路来进行修改。还可以添加更多锚点的位置,比如额头、耳朵等。

扩展阅读

本教程源码及后续更新

由于源码后续可能会更新,就不直接打包传在这里了。
本工程的持续更新源码可以在洪流学堂公众号回复face获取。


好了,今天就絮絮叨叨到这里了。
没讲清楚的地方欢迎评论,也可以加我微信讨论。

我是大智(VX: zhz11235),你的技术探路者,下次见!

别走!点赞收藏哦!

好,你可以走了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,001评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,210评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,874评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,001评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,022评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,005评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,929评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,742评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,193评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,427评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,583评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,305评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,911评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,564评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,731评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,581评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,478评论 2 352

推荐阅读更多精彩内容