国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

基于Unity和Vive眼動(dòng)SDK的VR眼動(dòng)追蹤研究場(chǎng)景開(kāi)發(fā)

這篇具有很好參考價(jià)值的文章主要介紹了基于Unity和Vive眼動(dòng)SDK的VR眼動(dòng)追蹤研究場(chǎng)景開(kāi)發(fā)。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問(wèn)。

基于Unity和Vive眼動(dòng)SDK的VR眼動(dòng)追蹤研究場(chǎng)景開(kāi)發(fā)

前言:因?yàn)楫厴I(yè)論文的需要,我得在一年內(nèi)盡快熟悉實(shí)驗(yàn)室的Vive pro eye并基于這套設(shè)備完成眼動(dòng)追蹤教育學(xué)注意力行為研究。感謝@Farewell弈和b站“鄧布利多軍”的先前工作,目前我的東西就是基于這兩位大佬的東西摸著石頭過(guò)河的。

跟隨本篇文章,你將學(xué)到如何在Unity開(kāi)發(fā)環(huán)境下,基于Vive pro eye硬件和SteamVR、OpenXR、SRanipaRuntime SDK三個(gè)第三方包,開(kāi)發(fā)出一個(gè)能實(shí)時(shí)獲取眼動(dòng)追蹤數(shù)據(jù)(包括3D視線碰撞坐標(biāo),2D屏幕下轉(zhuǎn)換坐標(biāo)、注視物體名稱(chēng)、時(shí)間戳等)的UnityVR場(chǎng)景,為之后的VR環(huán)境下眼動(dòng)追蹤研究提供參考。

需要注意的是,目前VR設(shè)備的眼動(dòng)追蹤能力只能和中低端眼動(dòng)儀設(shè)備相媲美,最高采樣率普遍為100hz左右(某些高端眼動(dòng)儀能達(dá)到上千hz),對(duì)于VR教學(xué)研究、一般的心理學(xué)研究已經(jīng)足夠,但是不太適合用于精度要求較高的研究題目(如研究“微眼動(dòng)”的心理學(xué)課題)。

環(huán)境配置

太長(zhǎng)不看版:
Unity 2021.3.19版本 + 三個(gè)第三方包:SteamVR、OpenXR、SRanipaRuntime SDK 1.3.3.0版(一定要1.3版本)

Unity的安裝就不說(shuō)了,都是些最為基礎(chǔ)的東西

筆者使用的Unity版本為2021.3.19

第三方包方面,需要準(zhǔn)備SteamVR、OpenXR,還有一個(gè)SRanipaRuntime SDK,前兩者可以用Window——Package Manager導(dǎo)入,后者需要去Vive官網(wǎng)下載,因?yàn)閂ive已經(jīng)全面轉(zhuǎn)向基于OpenXR的開(kāi)發(fā),SRanipaRuntime SDK并未上線Unity商店。

具體流程就不造輪子了,請(qǐng)參考以下文章:

需要注意:SRanipaRuntime SDK需選用1.3.3.0版本,1.6版本我在導(dǎo)入時(shí)發(fā)生了報(bào)錯(cuò),在第一篇文章下也有人反應(yīng)新版本反而有兼容性問(wèn)題,回滾至1.3.3.0就能正常使用了

HTC Vive Pro eye 眼動(dòng)數(shù)據(jù)簡(jiǎn)單獲取

【VR】HTC VIVE pro eye + Unity 眼動(dòng)注視軌跡可視化方案二

其實(shí)現(xiàn)在用SRanipaRuntime SDK是有點(diǎn)過(guò)時(shí)的選擇,如果可以的話建議換用OpenXR的XR_EXT_eye_gaze_interaction拓展,但是查了一圈國(guó)內(nèi)暫時(shí)沒(méi)有基于這個(gè)插件的實(shí)現(xiàn)工程,我就先求穩(wěn)用的SRanipaRuntime SDK了。

但是考慮到OpenXR統(tǒng)一化了大多數(shù)主流VR設(shè)備的開(kāi)發(fā)環(huán)境這點(diǎn)來(lái)看,轉(zhuǎn)向OpenXR+SteamVR在未來(lái)幾年是有必要的,屆時(shí)只要是支持OpenXR的硬件設(shè)備,就可以使用基于該開(kāi)發(fā)環(huán)境做出來(lái)的工程,不用再擔(dān)心兼容性問(wèn)題(考慮到這是VR硬件大廠們牽頭推廣的東西,這個(gè)概率很大),我也在考慮等這個(gè)demo開(kāi)發(fā)差不多后將SRanipaRuntime SDK轉(zhuǎn)成XR_EXT_eye_gaze_interaction

熟悉環(huán)境

太長(zhǎng)不看版:
SteamVR的Intractable Simple場(chǎng)景可用于快速熟悉SteamVR下的預(yù)制件;SRanipaRuntime SDK則有一個(gè)EyeSample_V2,兩者是后續(xù)開(kāi)發(fā)的基礎(chǔ)

Vive pro eye自帶眼動(dòng)追蹤校準(zhǔn)程序,體驗(yàn)前建議運(yùn)行,確保數(shù)據(jù)準(zhǔn)確

SteamVR熟悉

Unity資源管理器里通過(guò)SteamVR——InteractionSystem——Samples——Interaction_Examples.unity,可以找到SteamVR的交互預(yù)制件與演示合集,基本上之后想實(shí)現(xiàn)什么樣的功能都可以從這里找原型,不需要真的從頭造輪子。

這個(gè)場(chǎng)景自身也是可玩的,有弓、遙控車(chē)、手榴彈等等。

如果對(duì)場(chǎng)景需求的質(zhì)量要求不高的話,可以直接復(fù)制該場(chǎng)景進(jìn)行開(kāi)發(fā)。

SRanipaRuntime SDK熟悉

ViveSR——Scenes——Eye——EyeSample_v2.unity

也是個(gè)進(jìn)去后只要沒(méi)報(bào)錯(cuò)就直接能運(yùn)行的場(chǎng)景,其中比較重要的組件的GazeRaySample,我也是根據(jù)@鄧布利多軍大佬的想法,爆改了相關(guān)組件,以實(shí)現(xiàn)獲取數(shù)據(jù)的效果。

如果需要在其它場(chǎng)景中使用,搬運(yùn)SRanipal Eye Framework和Gaze Ray Sample兩個(gè)組件即可。

需求確定與實(shí)現(xiàn)

太長(zhǎng)不看版:用了一個(gè)取巧(偷懶)的辦法,爆改眼動(dòng)追蹤SDK的Gaze_Ray_Sample.cs,使其能輸出數(shù)據(jù),然后基于C#和python腳本,實(shí)現(xiàn)了辨析注視點(diǎn)、AOI可視化、動(dòng)態(tài)熱點(diǎn)圖可視化等研究需求

我的畢業(yè)論文需要在VR教學(xué)環(huán)境下實(shí)現(xiàn)采集眼動(dòng)追蹤數(shù)據(jù)并且進(jìn)行簡(jiǎn)單的分析,分析可以完全人工進(jìn)行,但是難點(diǎn)在于如何實(shí)現(xiàn)VR環(huán)境的眼動(dòng)追蹤數(shù)據(jù)采集。目前大多數(shù)眼動(dòng)追蹤實(shí)驗(yàn)都是基于2d平面(屏幕)進(jìn)行的,3d環(huán)境中的研究很少,GitHub倒是有個(gè)專(zhuān)門(mén)研究這個(gè)的pupil labs,但是他們的軟件需要購(gòu)買(mǎi)額外的硬件設(shè)備,也就是“軟件免費(fèi),硬件收費(fèi),兩者捆綁”的模式。

最后實(shí)在沒(méi)辦法,我自己想辦法實(shí)現(xiàn)了一下。思路放在這里,供大家參考。

unity 眼球追蹤,Unity、VR、眼動(dòng)追蹤開(kāi)發(fā),unity,vr,游戲引擎

根據(jù)找到的不多的文獻(xiàn)來(lái)看,至少得實(shí)現(xiàn)以上6個(gè)需求

凝視點(diǎn)是收集數(shù)據(jù)時(shí)最基本的單位;

注視點(diǎn)可視為用戶(hù)視線聚焦在某處超過(guò)某個(gè)值時(shí)(一般為200ms左右),即可視為在“注視”該物體;

感興趣區(qū)域(AOI)由研究者自行設(shè)置,主要研究多個(gè)用戶(hù)在實(shí)驗(yàn)時(shí)視線落在不同AOI處有無(wú)視覺(jué)規(guī)律或者其它現(xiàn)象;

熱點(diǎn)圖則是眼動(dòng)追蹤最直觀的可視化方式之一,同時(shí)也是實(shí)現(xiàn)時(shí)的難點(diǎn),unity不自帶實(shí)現(xiàn)方法,需要考慮動(dòng)用python。

unity 眼球追蹤,Unity、VR、眼動(dòng)追蹤開(kāi)發(fā),unity,vr,游戲引擎

最后決定分為兩大塊開(kāi)發(fā),原始數(shù)據(jù)獲取和數(shù)據(jù)處理部分,以實(shí)現(xiàn)上方的6個(gè)需求

不過(guò)實(shí)際開(kāi)發(fā)時(shí),“數(shù)據(jù)處理部分”又細(xì)分成“數(shù)據(jù)集生成”與“數(shù)據(jù)可視化”兩塊。

獲取原始數(shù)據(jù)

unity 眼球追蹤,Unity、VR、眼動(dòng)追蹤開(kāi)發(fā),unity,vr,游戲引擎

獲取原始數(shù)據(jù)分兩部分:一個(gè)是凝視的數(shù)據(jù)集,一個(gè)是所需的視頻

凝視的數(shù)據(jù)集的獲取方面,我則模仿了其它幾位大佬的做法,通過(guò)爆改SRanipal_GazeRaySample_v2.cs實(shí)現(xiàn),爆改后的代碼如下:

//========= Copyright 2018, HTC Corporation. All rights reserved. ===========
using System;
using System.IO;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.Assertions;

namespace ViveSR
{
    namespace anipal
    {
        namespace Eye
        {
            public class SRanipal_GazeRaySample_v2 : MonoBehaviour
            {
                public int LengthOfRay = 25;
                [SerializeField] private LineRenderer GazeRayRenderer;
                private static EyeData_v2 eyeData = new EyeData_v2();
                private bool eye_callback_registered = false;
                //增加變量
                private float pupilDiameterLeft, pupilDiameterRight;
                private Vector2 pupilPositionLeft, pupilPositionRight;
                private float eyeOpenLeft, eyeOpenRight;
                private string datasetFilePath;
                private StreamWriter datasetFileWriter;
                private float startTime;
                //增加變量結(jié)束
                public event Action<Vector3> CollisionPointEvent;
                //定義事件,以便將原始數(shù)據(jù)傳參給其他腳本

                private void Start()
                {
                    if (!SRanipal_Eye_Framework.Instance.EnableEye)
                    {
                        enabled = false;
                        return;
                    }
                    Assert.IsNotNull(GazeRayRenderer);

                    //
                    startTime = Time.time;
                    string format = "yyyy-MM-dd_HH-mm-ss";
                    string recordTime = System.DateTime.Now.ToString(format);
                    datasetFilePath = "dataset_" + recordTime + ".txt";
                    datasetFileWriter = File.AppendText(Path.Combine(UnityEngine.Application.dataPath, datasetFilePath));
                    UnityEngine.Debug.Log("Dataset file created at: " + Path.Combine(UnityEngine.Application.dataPath, datasetFilePath));
                    UnityEngine.Debug.Log("Recording started at: " + recordTime);
                    //
                }

                private void Update()
                {
                    if (SRanipal_Eye_Framework.Status != SRanipal_Eye_Framework.FrameworkStatus.WORKING &&
                        SRanipal_Eye_Framework.Status != SRanipal_Eye_Framework.FrameworkStatus.NOT_SUPPORT) return;

                    if (SRanipal_Eye_Framework.Instance.EnableEyeDataCallback == true && eye_callback_registered == false)
                    {
                        SRanipal_Eye_v2.WrapperRegisterEyeDataCallback(Marshal.GetFunctionPointerForDelegate((SRanipal_Eye_v2.CallbackBasic)EyeCallback));
                        eye_callback_registered = true;
                    }
                    else if (SRanipal_Eye_Framework.Instance.EnableEyeDataCallback == false && eye_callback_registered == true)
                    {
                        SRanipal_Eye_v2.WrapperUnRegisterEyeDataCallback(Marshal.GetFunctionPointerForDelegate((SRanipal_Eye_v2.CallbackBasic)EyeCallback));
                        eye_callback_registered = false;
                    }

                    Vector3 GazeOriginCombinedLocal, GazeDirectionCombinedLocal;

                    if (eye_callback_registered)
                    {
                        if (SRanipal_Eye_v2.GetGazeRay(GazeIndex.COMBINE, out GazeOriginCombinedLocal, out GazeDirectionCombinedLocal, eyeData)) { }
                        else if (SRanipal_Eye_v2.GetGazeRay(GazeIndex.LEFT, out GazeOriginCombinedLocal, out GazeDirectionCombinedLocal, eyeData)) { }
                        else if (SRanipal_Eye_v2.GetGazeRay(GazeIndex.RIGHT, out GazeOriginCombinedLocal, out GazeDirectionCombinedLocal, eyeData)) { }
                        else return;
                    }
                    else
                    {
                        if (SRanipal_Eye_v2.GetGazeRay(GazeIndex.COMBINE, out GazeOriginCombinedLocal, out GazeDirectionCombinedLocal)) { }
                        else if (SRanipal_Eye_v2.GetGazeRay(GazeIndex.LEFT, out GazeOriginCombinedLocal, out GazeDirectionCombinedLocal)) { }
                        else if (SRanipal_Eye_v2.GetGazeRay(GazeIndex.RIGHT, out GazeOriginCombinedLocal, out GazeDirectionCombinedLocal)) { }
                        else return;
                    }

                    Vector3 GazeDirectionCombined = Camera.main.transform.TransformDirection(GazeDirectionCombinedLocal);
                    GazeRayRenderer.SetPosition(0, Camera.main.transform.position);
                    GazeRayRenderer.SetPosition(1, Camera.main.transform.position + GazeDirectionCombined * LengthOfRay);

                    //以下為新增部分
                    //pupil diameter 瞳孔的直徑
                    pupilDiameterLeft = eyeData.verbose_data.left.pupil_diameter_mm;
                    pupilDiameterRight = eyeData.verbose_data.right.pupil_diameter_mm;

                    //pupil positions 瞳孔位置
                    //pupil_position_in_sensor_area手冊(cè)里寫(xiě)的是The normalized position of a pupil in [0,1],給坐標(biāo)歸一化了
                    pupilPositionLeft = eyeData.verbose_data.left.pupil_position_in_sensor_area;
                    pupilPositionRight = eyeData.verbose_data.right.pupil_position_in_sensor_area;

                    //eye open 睜眼
                    //eye_openness手冊(cè)里寫(xiě)的是A value representing how open the eye is,也就是睜眼程度,從輸出來(lái)看是在0-1之間,也歸一化了
                    eyeOpenLeft = eyeData.verbose_data.left.eye_openness;
                    eyeOpenRight = eyeData.verbose_data.right.eye_openness;

                    //UnityEngine.Debug.Log("左眼瞳孔直徑:" + pupilDiameterLeft + " 左眼位置坐標(biāo):" + pupilPositionLeft + "左眼睜眼程度" + eyeOpenLeft);
                    //UnityEngine.Debug.Log("右眼瞳孔直徑:" + pupilDiameterRight + " 右眼位置坐標(biāo):" + pupilPositionRight + " 左眼睜眼程度" + eyeOpenRight);

                    // 調(diào)用Physics.SphereCast進(jìn)行檢測(cè),并返回是否有碰撞產(chǎn)生
                    RaycastHit hit;
                    bool isHit = Physics.SphereCast(Camera.main.transform.position, 0.1f, GazeDirectionCombined.normalized, out hit, LengthOfRay);
                    string timestamp = (Time.time - startTime).ToString();
                    if (isHit)
                    {
                        // 碰撞到物體,返回碰撞點(diǎn)的坐標(biāo)
                        Vector3 collisionPoint = hit.point;
                        UnityEngine.Debug.Log("相交物體:" + hit.collider.gameObject.name);
                        //UnityEngine.Debug.Log("碰撞點(diǎn)坐標(biāo):" + collisionPoint);

                        // 觸發(fā)事件并傳遞碰撞點(diǎn)坐標(biāo)
                        CollisionPointEvent?.Invoke(collisionPoint);

                        // Write the data to the dataset file
                        datasetFileWriter.WriteLine(hit.collider.gameObject.name + "," +
                            collisionPoint + "," +
                            pupilDiameterLeft + "," +
                            pupilDiameterRight + "," +
                            timestamp);
                    }
                    else
                    {
                        // 未碰撞到物體
                        UnityEngine.Debug.Log("未發(fā)生碰撞");
                    }
                }
                private void Release()
                {
                    if (eye_callback_registered == true)
                    {
                        SRanipal_Eye_v2.WrapperUnRegisterEyeDataCallback(Marshal.GetFunctionPointerForDelegate((SRanipal_Eye_v2.CallbackBasic)EyeCallback));
                        eye_callback_registered = false;
                    }
                }
                private static void EyeCallback(ref EyeData_v2 eye_data)
                {
                    eyeData = eye_data;
                }
            }
        }
    }
}

視頻錄制反而花了不少時(shí)間:似乎是VR場(chǎng)景的渲染模式和一般的3D場(chǎng)景是不同的,而且根據(jù)研究需要,得在實(shí)驗(yàn)場(chǎng)景中設(shè)置兩個(gè)攝像機(jī),一個(gè)是玩家正常游玩視角,一個(gè)是固定的錄制視角(用于后期輸出動(dòng)態(tài)熱點(diǎn)圖視頻),雙攝像機(jī)有個(gè)坑點(diǎn):需要設(shè)置渲染順序,即Camera的depth值,不可設(shè)置成一樣,否則兩個(gè)攝像機(jī)都無(wú)法工作。
(后來(lái)查了一下這個(gè)也是3D游戲里制作抬頭顯示的方法——設(shè)置多個(gè)攝像機(jī)跟隨玩家視角,其中一個(gè)是專(zhuān)用的UI攝像機(jī),通過(guò)調(diào)整渲染順序的方式實(shí)現(xiàn)。)

Unity自帶的UnityRecorder無(wú)法正常工作。最后換用了AVPro Recorder,該軟件需要付費(fèi),我就不砸錢(qián)了,換用的破解版()

數(shù)據(jù)集生成

注視點(diǎn)的識(shí)別是我托幾位學(xué)弟完成的,
其原理為處理數(shù)據(jù)集,然后數(shù)據(jù)集中只要在某個(gè)值內(nèi)超過(guò)一定時(shí)間便視為注視點(diǎn)。

import math

#打開(kāi)文件并讀取
fin=open('dataset1.txt','r')
fout0=open('first_time.txt','w')
fout1=open('gazepoints.txt','w')
lines=fin.readlines()  #讀取整個(gè)文件所有行,保存在 list 列表中
#遍歷lines列表進(jìn)行數(shù)據(jù)處理
set0 =set()
gazingtime=0.0
distance=1.0
num=0
list1=str.split(lines[0],',')
print(list1)
x0 = float(list1[1][1:])
y0 = float(list1[2])
z0 = float(list1[3][0:-1])
time0 = float(list1[-1])
object_name0=list1[0]
print("{} {} {} {}".format(x0,y0,z0,time0))
for line in lines:
    list0=str.split(line,',')
    # print(list0)
    # print(list0[1][1:],end=' ')
    # print(list0[2],end=' ')
    # print(list0[3][0:-1])
    # print(type(float(list0[2])))
    #1.找到首次看到的物體及時(shí)間,并寫(xiě)入first_time.txt文件中
    if list0[0] not in set0:
        set0.add(list0[0])
        fout0.write("{} {}".format(list0[0],list0[-1]))
    #2.找出凝視點(diǎn),并寫(xiě)入gazepoints.txt文件中
    x1=float(list0[1][1:])
    y1=float(list0[2])
    z1=float(list0[3][0:-1])
    time1=float(list0[-1])
    # print("{} {} {} {}".format(x1, y1, z1, time1))
    object_name1=list0[0]
    distance=math.sqrt(pow(x1-x0,2)+pow(y1-y0,2)+pow(z1-z0,2))
    # print(distance)
    # print(time1-time0)
    if distance <=0.1 and object_name1==object_name0:
        detletime=time1-time0
       # print(detletime)
        gazingtime=gazingtime+detletime
        #print(gazingtime)
    elif distance>0.1:
        if gazingtime>=0.2:
            num=num+1
            print('{} {}'.format(object_name0, gazingtime))
            fout1.write("{}.{} {}\n".format(num,object_name0,gazingtime))
        gazingtime=0.0
        #print(1)
    elif  object_name1!=object_name0 and gazingtime>=0.2:
        num=num+1
        print('{} {}'.format(object_name0, gazingtime))
        fout1.write("{}.{} {}\n".format(num, object_name0, gazingtime))
        gazingtime = 0.0
    # if  object_name1 != object_name0 and gazingtime>=0.2 :
    #     print('{} {}'.format(object_name0,gazingtime))
    #     gazingtime=0.0

    x0=x1
    y0=y1
    z0=z1
    time0=time1
    object_name0=object_name1
    # print("{} {} {} {}".format(x0, y0, z0, time0))

fin.close()
fout0.close()

其實(shí)我覺(jué)得這個(gè)東西最好是在采集數(shù)據(jù)的同時(shí)判斷+記錄,不過(guò)似乎采集數(shù)據(jù)結(jié)束后再處理也是可以的,就先用著
順帶,基于tag獲取的數(shù)據(jù)缺乏嚴(yán)謹(jǐn),之后我會(huì)想辦法搞定這個(gè)。

AOI還在施工中,初步設(shè)想了兩種方案:一種是給場(chǎng)景內(nèi)所有物體增加tag,通過(guò)視線碰撞時(shí)識(shí)別tag實(shí)現(xiàn),一種是劃定3D空物體,識(shí)別視線穿過(guò)的第一個(gè)空物體,然后輸出該物體的名稱(chēng)。

工程示范:簡(jiǎn)單物理實(shí)驗(yàn)環(huán)境下的眼動(dòng)追蹤

注:搭建的實(shí)驗(yàn)環(huán)境可能并不嚴(yán)謹(jǐn),不過(guò)設(shè)計(jì)嚴(yán)謹(jǐn)?shù)膶?shí)驗(yàn)并不在該題目的研究范疇中。

依舊在施工中,會(huì)在未來(lái)更新文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-760161.html

到了這里,關(guān)于基于Unity和Vive眼動(dòng)SDK的VR眼動(dòng)追蹤研究場(chǎng)景開(kāi)發(fā)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來(lái)自互聯(lián)網(wǎng)用戶(hù)投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

覺(jué)得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包