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),你的技術探路者,下次見!

別走!點贊收藏哦!

好,你可以走了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章