最近项目用到了燧光MR眼睛(Pro版本)进行项目开发,其中有个功能点是要在离线模式下进行识别人脸、车牌、实物的功能。
一开始打算跟hololens一样使用vuforia来进行制作,但是查阅了vuforia最新的版本后才知道很早之前vuforia就已经不允许用户自行设置相机索引值了,vuforia之前有0和1两个索引值,最新的好像只有0了。但是燧光pro版眼镜的后置摄像头索引却是2。因此在测试完vuforia新旧版本后,只能放弃。
后面也查阅了其他方式想进行例如机器学习之类的方式进行人脸、车牌识别,从国外网站查阅了大量的案例(查了一周多),下载下载的案例要么没法使用,要么一堆报错,再或者要通过python先进行机器学习(本人不会python,看案例看的一头雾水)。无奈最终放弃了这个想法。
后来在论坛中看到了有人用EasyAR做出来了这个功能,于是我也尝试用EasyAR进行了测试,还是能达到一定的效果。不过在后续测试过程中,还是有一些问题,我将在最后提出来。
下载EasyAR插件
找到EasyAR插件网址,下载最新版本的EasyAR插件(这个网上一大堆教程,可以去自行搜索),下载完毕后,将EasyAR的Package包导入到unity中,在EasyAR官网中创建一个项目并且获取项目的License Key,将License Key放入到Project Settings的EasyAR中。
EasyAR配置
关于EasyAR教程方面我推荐看一篇文章,点击此处,我这里主要是讲一下关于燧光MR眼镜如何适配EasyAR的内容。
第一点:因为燧光MR眼镜的前置摄像机索引为2,因此我们需要对相机的索引在代码里进行设置
private CameraDeviceFrameSource cameraDevice;
//进入离线识别场景
private void OpenOfflineRecognitationScene()
{
ARSession Session;
Session = GameObject.FindObjectOfType<ARSession>();
cameraDevice = Session.GetComponentInChildren<CameraDeviceFrameSource>();
ChangeCameraIndex();
}
public void ChangeCameraIndex()
{
//if (!cameraDevice || cameraDevice.Device == null)
//{
// cameraNumberText.text = "错误1" + cameraDevice.ToString();
// cameraNumberText.text = "错误2" + cameraDevice.Device.ToString();
// return;
//}
if (CameraDevice.cameraCount() == 0)
{
//GUIPopup.EnqueueMessage("Camera unavailable", 3);
cameraDevice.Close();
return;
}
//var index = cameraDevice.Device.index();
//index = (index + 1) % CameraDevice.cameraCount();
cameraDevice.CameraOpenMethod = CameraDeviceFrameSource.CameraDeviceOpenMethod.DeviceIndex;
cameraDevice.CameraIndex = 2;
//GUIPopup.EnqueueMessage("Switch to camera index: " + index, 3);
cameraDevice.Close();
cameraDevice.Open();
}
上面的代码表示的是在我从一个非离线识别场景跳转到离线识别场景时,首先判断当前设备的相机数量,如果大于0,将摄像机的索引设置为2(本身燧光Pro眼镜的最大索引值应该就是2)。这时候眼镜中就会显示前方的画面。
这是第一步。这样眼镜就能开启后置摄像头了。
第二点:上面的教程中是在EasyAR插件里Image Target物体中的ImageTargetController类暴露出的字段中手动输入StreamingAssets里的路径来获取图片内容,但是一般我们项目中关于识别的物体都需要给留出一个接口来进行后续的图片替换、删除、添加。并且由于眼镜是安卓系统,我们无法直接在设备中打开StreamingAssets文件夹,并且StreamingAssets在安卓设备下是只读的,所以我们需要通过代码,动态的获取图片名称并且生成识别的内容。
动态创建EasyAR的Image Target物体并通过代码指定路径
首先我们需要知道安卓设备的持久化路径:Application.persistentDataPath
这个路径是读写都可以。我们将.apk文件安装到设备上并运行设备后,会自动创建的一个文件夹。后续我们想要增加、替换、删除图片的话可以直接都在这个文件夹中完成操作。
注:配置文件也可以通过这个文件夹访问到。
通过WWW的方式进行读取和设置配置文件
/// <summary>
/// 配置文件读取器
/// </summary>
public class ConfigurationReader
{
/// <summary>
/// 读取配置文件
/// </summary>
/// <param name="fileName">文件名</param>
/// <returns></returns>
public static string ReadConfig(string fileName)
{
//1. 因为 Application.streamingAssetsPath 在安卓平台下有时候无法读取资源
// 所有在开发过程中,使用自定义路径(unity 宏标签).
string path;
#if UNITY_EDITOR || UNITY_STANDALONE
path = "file://" + Application.dataPath + "/StreamingAssets/" + fileName;
#elif UNITY_ANDROID
path ="jar:file://"+ Application.dataPath + "!/assets/" + fileName;
#elif UNITY_IPHONE
path ="file://"+ Application.dataPath + "/Raw/" + fileName;
#endif
//2. 因为 System.IO.FileStream 只能操作明确的文件,而手机当中的文件没有明确路径.
//所以使用WWW类读取.
//例如:window
// D:\u3d1809\Month03\工程
//手机:
// ...\工程
//所以使用WWW类读取.
WWW www = new WWW(path);
//yield return www;//等待www读取完成
while (true)
{
if (www.isDone)
return www.text;
}
}
public static string ReadAndroidConfig(string fileName)
{
//1. 因为 Application.streamingAssetsPath 在安卓平台下有时候无法读取资源
// 所有在开发过程中,使用自定义路径(unity 宏标签).
string path;
#if UNITY_EDITOR || UNITY_STANDALONE
path = "file://" + Application.dataPath + "/StreamingAssets/" + fileName;
#elif UNITY_ANDROID
path ="jar:file://" + fileName;
#elif UNITY_IPHONE
path ="file://"+ Application.dataPath + "/Raw/" + fileName;
#endif
//2. 因为 System.IO.FileStream 只能操作明确的文件,而手机当中的文件没有明确路径.
//所以使用WWW类读取.
//例如:window
// D:\u3d1809\Month03\工程
//手机:
// ...\工程
//所以使用WWW类读取.
WWW www = new WWW(path);
//yield return www;//等待www读取完成
while (true)
{
if (www.isDone)
return www.text;
}
}
下面的类是创建Image Target物体,动态的给ImageTargetController脚本中的图片名称和图片路径赋值
using Common;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using easyar;
//这个类是用于读取配置文件、创建离线识别图片物体
public class TrackController : MonoBehaviour
{
// Start is called before the first frame update
public static Dictionary<string, string> trackMap;//
public static Dictionary<string, string> trackContentMap;//
public static Dictionary<string, AudioClip> sourceClipMap;
//ImageTrackerFrameFilter imageTrackerFrameFilter;
void Start()
{
//imageTrackerFrameFilter = GameObject.Find("Image Tracker").GetComponent<ImageTrackerFrameFilter>();
trackMap = new Dictionary<string, string>();
trackContentMap = new Dictionary<string, string>();
string content = ConfigurationReader.ReadAndroidConfig(Application.persistentDataPath + "/离线识别/配置文件/TrackGameObjectConfig.txt");
ConfigurationReader.LoadConfig(content, ParseLine);
content = ConfigurationReader.ReadAndroidConfig(Application.persistentDataPath + "/离线识别/配置文件/InformationConfig.txt");
ConfigurationReader.LoadConfig(content, ParseInformationLine);
//CreateTrackGameObject();
}
private void ParseInformationLine(string line)
{
line = line.Trim();
if (line == "") return;
string[] lines = line.Split('&');
trackContentMap.Add(lines[0], lines[1]);
}
public bool isTracking = true;
public float timer;
private void ParseLine(string line)
{
line = line.Trim();
if (line == "") return;
string[] lines = line.Split('&');
trackMap.Add(lines[0], lines[1]);
}
public void CreateTrackGameObject(ImageTrackerFrameFilter imageTrackerFrameFilter)
{
string path = Application.persistentDataPath + "/离线识别";
string[] dirArray = Directory.GetDirectories(path);//车牌、人物、物体识别、配置文件
foreach (var dir in dirArray)
{
string[] dirSplitArray = dir.Split('/');
GameObject trackTitle = new GameObject(dirSplitArray[dirSplitArray.Length - 1]);
if (dir.Contains("配置文件"))
continue;
//在这里得到3个识别的主体
string trackPath = dir;
string[] trackGoArray = Directory.GetDirectories(trackPath);//在这里得到每个主体文件夹中需要跟踪的物体列表
foreach (var trackGo in trackGoArray)
{
string[] trackGoSplitArray = trackGo.Split('/');
GameObject trackGameObjectParent = new GameObject(trackGoSplitArray[trackGoSplitArray.Length - 1]);
trackGameObjectParent.transform.parent = trackTitle.transform;
//在这里得到最终要识别的图片所在的文件夹路径
string picturePath = trackGo;
string[] pictureNames = Directory.GetFiles(picturePath);//在这里得到每个要识别的图片
for (int i = 0; i < pictureNames.Length; i++)
{
string[] pictureArray = pictureNames[i].Split('/');
string[] trackGoName = pictureArray[pictureArray.Length - 1].Split('.');
GameObject trackGameObject = new GameObject(trackGoName[0]);
trackGameObject.transform.parent = trackGameObjectParent.transform;
ImageTargetController controller = trackGameObject.AddComponent<ImageTargetController>();
controller.ImageFileSource.PathType = PathType.Absolute;
controller.ImageFileSource.Name = trackGoName[0];
controller.ImageFileSource.Path = pictureNames[i];
controller.Tracker = imageTrackerFrameFilter;
trackGameObject.AddComponent<Demo1>();
}
}
}
}
}
下面的类是创建完成Image Target后,给每个Image Target都添加上这个类,用于识别图片
using Crosstales.RTVoice;
using Crosstales.RTVoice.Tool;
using easyar;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
public class Demo1 : MonoBehaviour
{
private TrackController trackController;
void Awake()
{
trackController = GameObject.FindObjectOfType<TrackController>();
GetComponent<ImageTargetController>().TargetFound += ImageTargetController_TargetFound;
//加入丢失识别图的方法
GetComponent<ImageTargetController>().TargetLost += ImageTargetController_TargetLost;
}
public static Coroutine Ienum;
//丢失物体时执行
private void ImageTargetController_TargetLost()
{
Debug.Log("丢失");
TrackingPanel trackingPanel = UIManager.GetInstance.OpenPanel<TrackingPanel>();
trackingPanel.ShowMessage(trackingPanel.content, 5);
}
//识别到物体时执行
private void ImageTargetController_TargetFound()
{
TrackingPanel trackingPanel = UIManager.GetInstance.OpenPanel<TrackingPanel>();
StopCoroutine(trackingPanel.HideMessage(0));
StartCoroutine(JudgeShowTargetName());
}
public IEnumerator HideMessage()
{
yield return new WaitForSeconds(3);
UIManager.GetInstance.ClosePanel<MessagePanel>();
}
public static string goName;//扫描到的物体名称
private IEnumerator JudgeShowTargetName()
{
string goName = GetComponent<ImageTargetController>().ImageFileSource.Name;
string text = TrackController.trackMap[goName];
string content = TrackController.trackContentMap[text];
if (content == "")
{
UIManager.GetInstance.OpenPanel<MessagePanel>().ShowMessage("未找到对应的识别信息,请检查配置文件是否有误");
MonoBehaviourHelper.StartCoroutine(HideMessage());
yield break;
}
TrackingPanel trackingPanel = UIManager.GetInstance.OpenPanel<TrackingPanel>();
trackingPanel.ShowMessage(content, 5);
if (Demo1.goName == text)
{
yield break;
}
Demo1.goName = text;
//在这里也要得到配置文件里面的语音播报内容
//在这里实时获取音频片段
string sourceClipPath = "jar:file://" + Application.persistentDataPath + "/音频内容";
//在这里获取对应的音频片段
if (TrackController.sourceClipMap == null)
TrackController.sourceClipMap = new Dictionary<string, AudioClip>();
if (!TrackController.sourceClipMap.Keys.Contains(text))
{
WWW www = new WWW(sourceClipPath + "/" + text + ".mp3");
//yield return www;//等待www读取完成
while (true)
{
if (www.isDone)
{
AudioClip audioClip = www.GetAudioClip(true,true,AudioType.MPEG);
TrackController.sourceClipMap.Add(text, audioClip);
GameObject.Find("AudioSource").GetComponent<AudioSource>().clip = audioClip;
GameObject.Find("AudioSource").GetComponent<AudioSource>().Play();
yield return null;
}
}
}
else
{
AudioClip audioClip = TrackController.sourceClipMap[text];
GameObject.Find("AudioSource").GetComponent<AudioSource>().clip = audioClip;
GameObject.Find("AudioSource").GetComponent<AudioSource>().Play();
}
}
}
上述功能完成后,离线识别的功能就完成了,但是还有一个问题会出现,就是燧光头显的UI面板会镜像显示。
给Camera上加上这个类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 把画面左右镜像
/// </summary>
public class FrontCameraInvertImage : MonoBehaviour
{
Camera camera;
public bool flipHorizontal;
void Awake()
{
camera = GetComponent<Camera>();
}
void OnPreCull()
{
camera.ResetWorldToCameraMatrix();
camera.ResetProjectionMatrix();
Vector3 scale = new Vector3(flipHorizontal ? -1 : 1, 1, 1);
camera.projectionMatrix = camera.projectionMatrix * Matrix4x4.Scale(scale);
}
void OnPreRender()
{
GL.invertCulling = flipHorizontal;
}
void OnPostRender()
{
GL.invertCulling = false;
}
}
然后EeayAR的设置如下
这样关于燧光眼镜的离线识别功能基本就实现了
注:仍然存在的一些问题:无论是用vuforia或者EasyAR,无可避免的就是关于识别图片时的周围环境问题(比如我想识别一个凳子,由于凳子的识别图片是白天拍摄的,但是晚上的时候识别就很困难或者根本识别不到),由于都是通过图片进行的识别,所以实际上会受到灯光、周围环境等问题的影响和干扰,因此识别一个物体需要大量的图片(多环境、多角度等),所以其实对于这种AR识别人脸、车牌等功能的话,其实用EasyAR或者Vuforia还是有一些弊端的