项目学习记录(四)--基于图像检测的AR语文识字应用

前言:

之前做了ARFoundation的项目,想做利用Vuforia进行一个对比,简单记录下我的编写流程

项目构思:

利用AR技术开发一个有助于小学生学习汉字的读音、字意、字形,了解相似字和偏旁部首。(最后只做了一个小demo哈哈)

项目设计:

总共有五张卡片,一张写有“春字”,可以发出声音,同时扫描卡片会出现“春”相关的模型,让学生了解春的读音和字意。另外两张卡片分别是“口”、“欠”的卡片,当一起扫描时,会合成“吹”字,为了让学生对偏旁部首感兴趣。还有两张卡片是“卷”和“券”,目的主要是让学生进行相似字的区别,五张卡片如图所示;

整体采取游戏任务式设计,每个模块作为一个任务(1.请找出“春”字 2.请用卡片拼出“吹”字 3. 选用卡片组词蛋___(juǎn)  4. 选用卡片组词奖____(quàn)),完成所有任务后,游戏通关,任务设计如图所示。

项目实施:

在unityHub里新建项目,命名为AR_Chinese character。点击Unity 菜单栏 window>Package Manager,如图所示,打开Package Manager对话框,如图所示,在下拉列表中找到Vuforia Engine AR,并安装。

新建一个场景,在菜单栏选择File>Save as,命名为AR_Chinese character,如图所示。

在Hierarchy窗口空白处右击,选择vuforia Engine>ARCamera,如图所示。这时候打开电脑的摄像头,点击运行,我们就可以看到画面了。

现在我们要添加需要作为图像识别的图片我们需要去官网去添加图片。选择刚刚创建的ARCamera,选择它组件中的“open Vuforia Engine configuration”,如图所示

使用该插件,我们需要它官网的许可证和下载我们上传图片后生成的Database,选择Add License进入官网,如图所示。单击Develop标签,首先单击Licese Manager下的Get Basic按钮,如图所示,随后获得密钥,并将其复制,输入到APP License Key中。

随后我们在对象数据库 Target Manager 中单击Add Database 按钮加载一个数据库加载一个识别数据库,在弹出的对话框中,输入识别图片库名称wenzikapian,Type选择Device即可,如图所示

随后选择Add Target,选择界面如图所示,其中,Type选择Single,就是简单的图片识别。File选项可以从本地计算机中选择之别图片的地址。为了建立Unity3D场景中的单位长度,场景中所有其他物体的大小是以这个值为参照建立的。Vuforia 中的单位长度是以米来计算的。输人之后.图片的高度会以这个宽度来自动计算。这个值可以是任意的。Name中输人识别图的名字,这个很重要,每张识别图对象都有一个唯一的名字,而且Vuforia可以同时识别多张不同的图片,因此如果以后要用代码来控制选择哪个对象的话,就是用这个名字来查找是哪张识别图。

下载文末素材链接中的AR识字压缩包中的AR识别图片并添加,随后效果如图所示,图中的星际代表了识别度的高低,从五颗星一次往下排序,五星识别度最好,其识别特征点也最多,便于测试。

选择Download Database(All),把资源导入到unity中。现在就可以使用我们自己的图片了。在Hierarchy窗口选择vuforia Engine>Image,如图所示。

选中ImageTarget,在Inspector面板中将Database设为wenzikapian,将MultiTarget设为chun,如图014所示,Sence演示效果如图所示。

现在我们导入有关春的模型,导入随书资源中的PolyWorks Full Pack.unitypackage。把AXeyWorks/PolyWorks/Full Pack/Demos中的Forest的场景文件拖入本场景,把其中的Sence游戏对象作为ImageTarget的子物体,调整大小和位置,最后把Forest场景文件移除,删除main Camera,如图所示,现在进行测试,当扫描到春字图片时,就可以有模型出现。

现在我们要实现触摸卡片有读音播放,这时就需要用到vuforia的虚拟按钮。选中ImageTarget,在其属性面板中找到Advanced属性,单击打开前面三角符号可以看到隐藏在其中的内容,最下部有Add Virtual Button,如图所示。单击该按钮创建一个虚拟按钮,并调节按钮的位置大小,按钮位置如图所示。

现在来实现虚拟按钮的功能,创建一个C#脚本,命名为VirtualButtonEventHandler

脚本内容如下:

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using Vuforia;

using UnityEngine.SceneManagement;

[RequireComponent(typeof(AudioSource))]

public class VirtualButtonEventHandler : MonoBehaviour,IVirtualButtonEventHandler{

    VirtualButtonBehaviour[] vbs; //多个按钮用数组保存

    private bool isRotate = false;

    [SerializeField]

    private AudioSource Ac;

//当按钮被按下时的相应事件

    public void OnButtonPressed(VirtualButtonBehaviour vb)

    {

        switch (vb.VirtualButtonName)

        {

            case "Read":

                Ac.Play();

                Debug.Log("c");

                break;

            default:

                break;

        }

    }


    public void OnButtonReleased(VirtualButtonBehaviour vb)

    {

        switch (vb.VirtualButtonName)

        {


            default:

                break;

        }

    }


//为虚拟按钮挂载脚本

    void Start () {

        vbs = this.GetComponentsInChildren<VirtualButtonBehaviour>();

        for (int i = 0; i < vbs.Length; i++) {

            vbs[i].RegisterEventHandler(this); // 把脚本注册到按钮上去

        }

        Ac = this.GetComponent<AudioSource>();

    }

   

}

这个脚本实现的功能主要是当虚拟按钮被按下时,如果Name为“Read”,就播放对应的音频,大家也可以尝试添加不同名称的虚拟按钮,实现不同的效果。

注:这里不同的版本可能会出问题可以参考下面这个网站进行代码的修改

【Unity小帮手】VuforiaAR解决虚拟按键IVirtuaButtonEventHandler停用问题_ar虚拟按键-CSDN博客

然后我们把代码挂载到ImageTarget上面,选择虚拟按钮进行命名,并选择随书资源中的春字的读音,对Audio Source进行赋值,取消勾线Audio Source的Play On Awake,如图所示,现在当扫描到图片,我们用手指遮挡住虚拟按钮,就会发出春字的读音。

现在我们来实现拼字的效果,主要利用的是距离检测和渲染检测。我们需要同时识别两个字,我们先修改vuforia最多能同时检测图片的个数,选择ARCamera,选择它组件中的“open Vuforia Engine configuration”,我们将Max Simultaneous Tracker images 设置为2,如图所示

现在我们在Hierarchy窗口再创建两个ImageTarget,设置图片为“口”和“欠”的卡片,同时找到随书资源中的“口”和“欠”的模型,分别作为对应卡片的子物体,再把“吹”字的模型拖入场景,然后导入资源中Volumetric Crystal Materials Pack 1.1.1.unitypackage材质包,为模型添加自己喜欢的材质,调整大小和位置,最后效果如图所示。

现在我们需要的是检测图片是否被检测到,如果两个图片都被检测到了,并且两个字靠的很近,就进行拼字操作,隐藏“口”和“欠”,显示“吹”字。我们这里不能用检测游戏对象activeInHierarchy属性来进行判断图片是否被识别,因为图片和模型在初始化后都是active的,只是没显示在屏幕前。因此我们要通过检测是否有游戏对象渲染在屏幕前,来判断图片是否被检测。

新建C#脚本,命名为HanziRenderer。

脚本内容如下:

using System.Collections;

using System.Collections.Generic;

using UnityEngine;


public class HanziRenderer : MonoBehaviour {

    public  bool isFound = false;

    public void OnBecameVisible()//当模型进入屏幕,只对有渲染组件的物体有效

    {

         isFound = true;

    }

    public void OnBecameInvisible()//当模型退出屏幕,只对有渲染组件的物体有效

    {

         isFound = false;

    }

}

现在如果我们把脚本挂载到字的模型上,考虑到我们最后要将字的模型隐藏掉,那么模型的脚本就不生效了,后续就不能检测图片是否被检测的状态了。所以我们创建一个空物体,我们把脚本挂载在这个空物体上,并为这个空物体添加渲染组件(Mesh Renderer),这样就算字的模型被隐藏也不会让脚本失效了。(总共要创建欠和口的两个空物体)如图所示:

现在我们创建用于拼字的脚本(主要是距离检测),命名为Hanzi_controller。

脚本内容如下:

using System.Collections;

using System.Collections.Generic;

using UnityEngine;


[RequireComponent(typeof(AudioSource))]

public class Hanzi_controller : MonoBehaviour {

    public HanziRenderer hanziRenderer1;

    public HanziRenderer hanziRenderer2;

    public AudioClip audioClip;//拼出的字的音频。

    protected AudioSource audioSource; //拼字时发出的声音

    public GameObject[] imageTarget;

    public GameObject[] Chinesetextgo;//两个偏旁部首组成一个字。

    private bool isPlay = false;

    public float TargetDistance; //设置什么距离就可以拼字

                                        

   

    public GameObject LastChineseText; //要拼的字。


    void Start() {

         audioSource = this.GetComponent<AudioSource>();

    }


    void Update() {

         if (hanziRenderer1.isFound && hanziRenderer2.isFound)

         {

             Debug.Log("具备拼字条件");

             hebing();

         }

         else {

             foreach (var item in Chinesetextgo)

             {

                  item.SetActive(true);

             }

             LastChineseText.SetActive(false);


         }



    }


    public void hebing()

    {

        if (distance(imageTarget[0].transform.position, imageTarget[1].transform.position) < TargetDistance)

       {

         Debug.Log("进入拼字范围");

             Invoke("show", 1);


       }

       else

       {

             Debug.Log("距离太远了");



        }




    }

    public double distance(Vector3 go1,Vector3 go2)

   {

        

         double result = 0;

         result = Mathf.Sqrt((go1.x - go2.x) * (go1.x - go2.x) + (go1.y - go2.y) * (go1.y - go2.y) + (go1.z - go2.z) * (go1.z - go2.z));

         return result;

    }


    public void show()

    {

         LastChineseText.SetActive(true);

         //如果直接用position,会把旋转也加进去

         LastChineseText.transform.position = new Vector3((Chinesetextgo[0].transform.position.x + Chinesetextgo[1].transform.position.x) / 2, (Chinesetextgo[0].transform.position.y + Chinesetextgo[1].transform.position.y) / 2, (Chinesetextgo[0].transform.position.z + Chinesetextgo[1].transform.position.z) / 2);

         foreach (var item in Chinesetextgo)

         {

             item.SetActive(false);

         }

         if (isPlay == false)

         {

             isPlay = true;

             audioSource.PlayOneShot(audioClip);

         }

    }

   



}

脚本比较关键的点在于,在本脚本中我们先获取两个挂载了HanziRenderer的物体,当两张图片都被检测到时,也就是两个挂载了脚本的空物体被检测到了,就满足了进行拼字的第一个条件。然后进行两张卡片距离的判断,如果距离近,就实现拼字的效果,显示所要拼的字,隐藏偏旁部首。

同时为了后期的扩展性(实现多种拼字),我们新建C#一个脚本,继承Hanzi_controller。

脚本内容如下:

using System.Collections;

using System.Collections.Generic;

using UnityEngine;


public class chui_controller :Hanzi_controller {


    // 可以添加一些自己想要的效果,比如说吹风的特效和音频


}

我们新建一个空物体,名为chui_controller,把脚本挂载在上面,Audio Clip属性选择资源包中Audio文件夹下的tishi,ImageTarget数组里放“口”和“欠”两个字的ImageTarget,Chinesetextgo数组里放“口”和“欠”的立体模型,LastChineseText赋值为“吹”的立体模型,如图所示

这样我们就实现了拼字的效果。最后要在测试中调整吹字模型的朝向和Target Distance,得到比较好的效果。

接下来我们来实现相似字的识别,同上先新建ImageTarget,选择对应“卷”和“券”的卡片,并把对应的字的模型作为其子物体,赋予喜欢的材质。现在我们需要检测用户是否用对了卡片,而且同时要明确用户选择了哪个问题。我们要实现的效果是当用户点击任务时,就确定目前的任务。

在Hierarchy窗口单击鼠标右键,选择UI>Canvas创建一个UI根节点,同时这个操作会自动创建一个EventSystem,用于处理UI事件。在Hierarchy窗口单击鼠标右键,选择UI>Image,命名为task1,将Image组件中的Source Image 属性设置为资源中的taskbg。选择task,单击鼠标右键,选择UI>Text,在文本框输入“请找出卡片的“春”字”。调整文字和图片大小。字体选择资源中的“可爱猫猫字体”。同上制作拼出吹字、卷和券识别的任务,分贝命名为task2,task3,task4。可以在Scene窗口中调整图像的位置,在Inspector窗口中选择对齐方式,这里4个任务我们选择居中对齐,如图所示。最后的效果如图所示。

现在我们要让任务可以点击,并且当点击了任务时,任务会到屏幕中央放大。

我们导入资源中的DoTween.unitypackage。选择task1,选择Add Component,为其添加button和两个Dotween Animation 组件,如图所示

我们选择一个Do Tween Animation ,选择“move”,如图所示,设置移动到

(0,0,0)的坐标,取消AutoPlay和AutoKill(自动播放和动画的自动销毁),如图所示。

随后我们选择另一个DOTween Animation组件,选择放大到自己的1.5倍,同时取消AutoPlay和AutoKill,如图所示,同时我们在envent中点击On Star,然后单击On Click()下面的“+”号按钮,指定移动的动画DOTween Animation作为消息的接受对象,选择添加移动动画DOTween Animation中的PlayForward()函数为做相应Scale放大开始事件的回调函数,如图所示。

 

随后选择Button组件然后单击On Click()下面的“+”号按钮,指定有Scale放大动画DOTweenAnimation的游戏对象作为消息接收对象,选择Scale放大动画DOTweenAnimation的PlayForward()函数函数作为响应按钮单击事件的回调函数,如图所示。

现在运行测试,我们就可以发现当我们点击task1的任务时,task1就会移动到中央并且放大。同上,我们用相同的步骤制作另外三个任务。

现在我们要实现当点击另外一个任务,之前点的任务就会恢复之前的动画。

创建一个C#脚本,命名为dotween_controller。

脚本内容如下:

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using DG.Tweening;


public class dotween_controller : MonoBehaviour {


    public DOTweenAnimation[] dOTweenAnimations;

    public void change(int index)

    {

        for (int i = 0; i < dOTweenAnimations.Length; i++)

        {

            if (i == index)

            {

                  dOTweenAnimations[i].DOPlayForward();

            }

            else

            {

                  dOTweenAnimations[i].DOPlayBackwards();

            }

            

        }

    }

}

这个脚本change(int)函数实现的效果是运行时只播放对应下标动画,把其他的动画回放(也就是回到初始位置)。

我们右击Canvas新建一个空物体,命名为dotween_controller,并挂载dotween_controller组件。并对其赋值,如图所示。

同时选择task1游戏对象的button组件,删除播放动画的事件。并指定dotween_controller作为消息接收对象,选择dotween_controller的change()函数作为相应按钮单击事件的回调函数。设置值为0.如图所示。其他的任务同上操作(但要设置对应下标的值)。

现在我们需要确定我们选择的是哪个题目,如果检测到图片就进行反馈。首先我们先创建检测到正确图片和检测到错误图片的反馈UI。

新建一个Text,重命名为successText,为文本添加outline组件,并调整位置、大小、颜色,并为其添加一个Image作为子物体,选择资源中的gift作为其Image的Source Image的属性。同时新建一个Text,重命名为failText,作为失败的反馈,添加outline组件,并调整位置、大小、颜色,并为其添加一个Image作为子物体,选择资源中的yanzi作为其Image的Source Image的属性,并把两个对象移动到画布之外,最后的效果参考如图所示。

正在上传…重新上传取消

同时我们来需要所有任务完成的反馈和重新开始的按钮。我们右击Canvas新建一个空物体,命名为successroot,新建Text,button作为它的子物体,并进行位置、大小、图片的调整。最后效果如图所示。

接下来,我们新建一个C#脚本,命名为timu_Controller,新建一个空物体,命名为timu_Controller,把该脚本挂载上去。

脚本的内容如下:

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using DG.Tweening;


public class timu_Controller : MonoBehaviour {



    public HanziRenderer[] hanziRenderers;


    public bool[] testarr;

    public bool[] isfinsh;//判断对应下标的任务是否完成。



    public DOTweenAnimation dOTweenAnimation1;//成功动画

    public DOTweenAnimation dOTweenAnimation2;//失败动画

    public static timu_Controller instance;


    public int index;

    public GameObject root;

    public GameObject canvas;

    public GameObject successroot;



    public timu_Controller Getinstance()

    {

         return instance;

    }

     void Awake()

    {

         instance = this;

    }

    void Start () {

        

    }

   

    void Update () {

         testListenter(index);


    }

   

public void testListenter(int index)

    {

        

        if (testarr[index])

        {

            if (hanziRenderers[index].isFound)

            {

                  dOTweenAnimation1.DOPlayForward();

                  testarr[index] = false;

                  isfinsh[index] = true;

            }

           

                for (int i = 0; i < hanziRenderers.Length; i++)

                {

                    if (i != index && hanziRenderers[i].isFound)

                    {

                          dOTweenAnimation2.DOPlayForward();

                    }

                }

           

        }

         juadgeFinsh();


    }



    public void change(int index)//修改当前的显示与隐藏

    {

        for (int i = 0; i < testarr.Length; i++)

        {

            if (i == index)

            {

                  testarr[i] = true;

            }

            else

            {

                  testarr[i] = false;

            }

        }

    }

    public  void  btnonclick(int index)//下标表示第几题

    {

         change(index);

         Debug.Log("目前题目是:"+index);

         this.index = index;

         Debug.Log(this.index);

    }


    public void juadgeFinsh()

    {

        for (int i = 0; i < isfinsh.Length; i++)

        {

             if (!isfinsh[i]) return;

        }

         root.SetActive(false);

         Invoke("successrootsetactive", 2f);

        

    }



    public void reset()

    {

         successroot.SetActive(false);



        for (int i = 0; i < isfinsh.Length; i++)

        {

             isfinsh[i] = false;

        }

         root.SetActive(true);

    }

    public void successrootsetactive()

    {


         successroot.SetActive(true);

    }

}

这个脚本实现的效果主要是在update中通过testListenter()函数不断检测选择的任务是否有对应的图片被检测到,如果是对应的图片就播放successText上挂载的动画,如果不是对应的图片就播放failText上挂载的动画。同时通过juadgeFinsh()函数检测是否isfinish数组都为true,也就是任务全部完成,如果全部完成,就隐藏root,显示successroot。

现在在Hierarchy窗口选择task1游戏对象,随后选择Button组件然后单击On Click()下面的“+”号按钮,指定timu_controller游戏对象作为消息接收对象,选择timu_controller的btnonclick()函数作为响应按钮单击事件的回调函数,来确定目前选择的题目下标,如图所示。其他三个任务同上操作,但要设置对应的下标。

现在我们选择successroot下的button,为其button挂载脚本中reset的方法,如图所示,目前是重置isfinisn数组的状态,恢复开始界面的显示。

现在我们开始成功和失败反馈的动画制作。我们选择successText游戏对象,为其添加DOTween Animation组件,添加移动到(0,0,0)的动画,取消它的AutoPlay和AutoKill,同时在点击OnComplete,点击“+”,指定本物体为消息对象,把其DOTween Animation的DOPlayBackwards()函数作为当本动画结束播放的回调函数,如图所示。失败的UI同理操作。

现在需要对timu_controller游戏对象进行赋值,在这之前,我们应该把Hanzi_Renderer脚本挂载到春的模型和卷的模型、券的模型、吹的模型上,让他们能够被脚本检测到。我们利用Hanzi_Renderer脚本中的isfound属性来判断图片是否被识别,注意我们还要为Sence游戏对象(有关“春”的模型)添加Mesh Renderer,因为渲染的组件才能被检测,如图所示。

 

最后进行赋值,注意Hanzi Renderers元素的顺序要根设计的题目的顺序一致,hanzi Renderers、Testarr、Isfinsh的数量要一致,如图所示。

现在点击运行,就能实现选择题目,实现对应反馈的效果了。

最后进行优化,我们现在如果在选择任务时,有UI显示,会不方便。我可以设计一个双击取消UI显示的脚本,我们新建一个空物体,命名为root,把Canvas上任务的游戏对象都挂载在这个空物体之下。创建一个C#脚本,命名为Canvas_uiController,挂载到Canvas上,把Root赋值为Canvas的子物体root,如图所示。

脚本内容如下:

using System.Collections;

using System.Collections.Generic;

using UnityEngine;


public class Canvas_uiController : MonoBehaviour {


    private float main_time,time, two_twoClicks;

    public float click_time;

    private float two_click_time;

    private int count;

    public GameObject root;

    // Use this for initialization

    void Start () {

        

    }

   

    // Update is called once per frame

    void Update () {

        if (Input.GetMouseButton(0))

        {

            if (main_time == 0.0f)

            {

                main_time = Time.time;

            }

            if (Time.time - main_time > 0.2f)

            {

                //长按时执行的动作放这里

            }

        }

        if (Input.GetMouseButtonUp(0))

        {

            if (Time.time - main_time < 0.2f)

            {//当鼠标抬起时,检测按下到抬起的时间,如果小于2.0f就判断为点击。

                Debug.Log("dianji");

                if (two_twoClicks != 0 && Time.time - two_twoClicks < 0.2f)

                {

                    count = 2;

                 

                }

                else

                {

                    count++;

                    if (count == 1)

                    {

                        time = Time.time;

                    }

                }

                if (count == 2

                 && ((time != 0 && Time.time - time < 0.2f) || (two_twoClicks != 0 && Time.time - two_twoClicks < 0.2f)))

                {//如果两次点击事件小于0.2f就判断为双击

                 //双击时执行的代码块

                    count = 0;

                    Debug.Log("shuangji");

                    root.SetActive(!root.activeInHierarchy);

                   

                }

                if (count == 2 && (Time.time - time > 0.2f || Time.time - two_twoClicks > 0.2f))

                {

                    two_twoClicks = Time.time;

                    count = 0;

                }

                main_time = 0.0f;

            }

            else

            {

                main_time = 0.0f;

            }

        }

    }

}

这个脚本实现是当我们双击屏幕时,我们就可以显示或者隐藏任务。

同时我们需要建一个UI,让用户知道这个操作。’因此我们需要做一个Tip的UI。我们右击Canvas,新建一个Button,命名为Tip,把资源中的tip作为其Image组件中Source Image的值。同时右击Tip、,新建一个作为其子物体,设置其属性,如图所示。随后选择Tip,指定Text为消息接受对象,选择Gameobject中的SetActive(bool)方法作为按钮按下的回调方法,如图所示,随后隐藏掉Text游戏对象。

然后我们再制作一个标题,新建一个Text,重命名为Title,新建一个Image作为Text的子物体,最后效果如图所示。

最后隐藏successroot游戏对象,把Title和Tip也放到root之下,作为其子物体,点击运行,可以测试项目的完整效果。

接下来,我们进行导出到手机的设置,选择安卓平台,并选择Switch Platform,如图所示。最后build就完成了。

项目测试效果如下:

unity3D Vuforia AR语文识字应用

总结:

对应小白而言,如果你想做的只是卡片识别一类的,Vuforia的功能是在这方面我感觉使用起来是门槛比较低的,ARFoundation的优势在于他的其他识别,比如说人脸、平面检测,有机会,我会去尝试一下。

源码链接:

链接:https://pan.baidu.com/s/1oznSD4Z4QhS6hJEzD-iJHw?pwd=fhoy
提取码:fhoy

素材链接:

链接:https://pan.baidu.com/s/19OI7k_ErC6hVM3U7ZI0Qcw?pwd=qz0m
提取码:qz0m

安装包链接:

链接:https://pan.baidu.com/s/1zLPMeghcBCHm0pH73lCdpg?pwd=1234
提取码:1234

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值