效果展示

整个项目主要分为三部分,一开始无限循环验证是否有狗狗照片,找到狗狗照片之后就进入狗子的MMD部分,狗子跳完病名为爱之后会有一场烟花,烟花之后是一封信【当然信部分删掉了233

TensorFlow 在Unity中引入

TensorFlow CSharp

TensorFlow得以成功的部署在Unity中完全得益于这个项目,现在CSharp版本的Tensorflow已经更新到1.5,支持和Python一样的各种操作,但是今天主要只使用其中的部署部分

在宇宙第一IDE中Nuget下载TensorFlow Cshap之后

using TensorFlow
var graph = new TFGraph();
 // 从文件加载序列化的GraphDef
var model = File.ReadAllBytes(modelFile);
//导入GraphDef
graph.Import(model, "");
using (var session = new TFSession(graph)){
    var tensor = CreateTensorFromImageFile(file);
    var runner = session.GetRunner();
    runner.AddInput(graph["input"][0], tensor).Fetch(graph["output"][0]);
    var output = runner.Run();
}

但是查阅了官方lib.so文件,TensorFlow Csharp的lib只有Mac, Windows, Linux三个平台,一开始尝试强制在Unity中使用lib,但是最后会报引用错误,索性最后找到了官方的ML-Agent中的TensorFlow库,支持所有平台【比心

所以整个开发流程就变成了,在宇宙第一IDE中跑通本地测试,然后再放入Unity中稍加修改就好

ML-Agent

这个引擎本来是Unity希望和Python联动的一个过程,今天我们忽略其中训练的部分,只采用部署部分

引入整个ML-Agent引擎之后,就可以

using TensorFlow

在安卓平台的开头要加上

##if UNITY_ANDROID
TensorFlowSharp.Android.NativeBinding.Init();
##endif

同时要注意,因为Unity自身特性,需要使用.bytes的图文件,其中要包含本身的pt图文件,也要包含checkpoint部分官方有合并的Demo,在此不再赘述。

导入图部分(只导入一次):

public Classifier(byte[] model, string[] labels){
##if UNITY_ANDROID
    TensorFlowSharp.Android.NativeBinding.Init();
##endif
    this.labels = labels;
    this.graph = new TFGraph();
    this.graph.Import(model, "");
}

识别部分:

 public Task<bool> IsDogAsync(Color32[] data){
    return Task.Run(() =>{

        using (var session = new TFSession(graph)){
             //采用Color32 pic的方式读取图片
             var tensor = TransformInput(data, INPUT_SIZE, INPUT_SIZE);
            //采用byte[]方式读取图片
            //var tensor = CreateTensorFromImageFile(data);
            var runner = session.GetRunner();
             runner.AddInput(graph[INPUT_NAME][0], tensor).Fetch(graph[OUTPUT_NAME][0]);
            var output = runner.Run();

            // output[0].Value() is a vector containing probabilities of
            // labels for each image in the "batch". The batch size was 1.
            // Find the most probably label index.
            var result = output[0];

            //return部分略掉了
        }
    });
}

注意调用识别部分全部采用的Task Async+InvokeRepeating模式,而Unity中自带的yield(比如定时5s执行一次)+IEnumerator就必须写在Update中(限制在主线程中),这样其实只是改变了运行的时间,而不是单独开一个线程运行,所以写在Update里,实际就还是在Update中执行,所以会造成明显卡顿,这里需要一直不断用多线程函数循环

InvokeRepeating(nameof(checkDog), 1f, 1f);
 private async void checkDog() {
    var snap = TakeTextureSnap();
    var scaled = Scale(snap);
    var rotated = await RotateAsync(scaled.GetPixels32(), scaled.width, scaled.height);
    bool isDog = await classifier.IsDogAsync(rotated);
    if (isDog) {
        DogTimes += 1;
    }
    if (DogTimes > 1) {
        IsMagicSystemStart = true;
        IsDogSystemStart = true;
    }
}

Unity中背景调用摄像头

目的

因为本身是想做成 AR 的,所以背景层是实时摄像头,同时又需要满足Tensorflow获取实时画面。

采用EasyAR + 截图方式 (Failed)

一开始是使用EasyAR的包,好处是不用自己考虑各平台调用摄像头的问题,但是查阅了EasyAR的手册并没有发现把当前帧输出出来的API,在论坛有人问过类似的问题,官方给出的答复是去参考他们的Demo【就不能直说么……】,看完他们的Demo发现其实是调用了截图,把当前背景的截图保存成Texture,于是尝试采用截图:

Unity自带的截图主要有两种:

1.直接调用内部API截图,但是及其慢,而且不能指定截图位置和大小【虽然在这里不需要指定大小】

Application.CaptureScreenshot("Screenshot.png", 0);  

2.把当前屏幕保存成一个Texture2D,然后把当前纹理保存成PNG

/// <param name="rect">Rect.截图的区域,左下角为o点</param>  
Texture2D CaptureScreenshot2(Rect rect)   
{  
    // 先创建一个的空纹理,大小可根据实现需要来设置  
    Texture2D screenShot = new Texture2D((int)rect.width, (int)rect.height, TextureFormat.RGB24,false);  

    // 读取屏幕像素信息并存储为纹理数据,  
    screenShot.ReadPixels(rect, 0, 0);  
    screenShot.Apply();  

    // 然后将这些纹理数据,成一个png图片文件  
    byte[] bytes = screenShot.EncodeToPNG();  
    string filename = Application.dataPath + "/Screenshot.png";  
    System.IO.File.WriteAllBytes(filename, bytes);  
    Debug.Log(string.Format("截屏了一张图片: {0}", filename));  

    // 最后,我返回这个Texture2d对象,这样我们直接,所这个截图图示在游戏中,当然这个根据自己的需求的。  
    return screenShot;  
}  

经过尝试确实比直接调用截图API快一些,但是在截图的时候还是有肉眼可查的明显的卡顿,【也可能是只在电脑上尝试,性能不够】

参考网上其他把TensorFlow部署到Unity里面的教程,使用了从相机Texture直接读取的方式

直接读取相机的Texuture获取图片(Succeed)

参考natsupy GithubGist
TextureTools中使用的

result.SetPixels(texture.GetPixels(Mathf.FloorToInt(xRect), Mathf.FloorToInt(yRect),
                                    Mathf.FloorToInt(widthRect), Mathf.FloorToInt(heightRect)));

是直接使用相机的texture,实测速度优于从把相机的texture输出到底层屏幕,再截图底层屏幕的速度。
【为什么同样截取texture,从相机输入会比屏幕更快……】

Unity调用相机的那些坑

本身调用相机规规矩矩的使用用WebCamDevice就好

WebCamDevice[] devices = WebCamTexture.devices;

if (devices.Length == 0){
    camAvailable = false;
    return;
}

backCam = new WebCamTexture(devices[0].name, Screen.height, Screen.width);
background.texture = backCam;
backCam.Play();
camAvailable = true;

但是在安卓上直接显示在背景上是有问题的,获取的相机实际图像其实是旋转90°,比如实际竖屏(X,Y) 1080 x 1920, 然而安卓相机获取的却是 1920 x 1080 竖着的相机图像 ,并且会自动旋转90°再放到屏幕上(1080x1980)……所以最后的效果是满屏的摄像头图像,但是是旋转九十度的。
【这里网上有把 height 和 width输入时反置的,得到 1080 x 1920竖屏图像,但是之后还是会旋转90°,变成1920x1080,会造成上下显示不全,但是这样后续只需要逆向旋转90°就可以了】

所以如果我们希望得到竖屏的图像,需要把整个背景图像旋转90°,但是没完,这样得到的图像是 1920 x 1080 的竖屏图像,显示出来就是 1080 x 1080的 图像,之后还需要进行等比缩放,把原来1920x1080中的 1080 缩放到 1920

//form youtube:https://www.youtube.com/watch?v=c6NXkZWXHnc
//     github :https://github.com/Chamuth/unity-webcam/blob/master/MobileCam.cs
float ratio = (float)backCam.width / (float)backCam.height;
fitter.aspectRatio = ratio; // Set the aspect ratio
float scaleY = backCam.videoVerticallyMirrored ? -1f : 1f; // Find if the camera is mirrored or not
background.rectTransform.localScale = new Vector3(1f, scaleY, 1f); // Swap the mirrored camera
int orient = -backCam.videoRotationAngle;
background.rectTransform.localEulerAngles = new Vector3(0, 0, orient);
background.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, Screen.height);
background.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, Screen.width);

烟花部分

网上所有的烟花都是基于js的unity4.x版本……各个资源站都是传来传去的,就是Unity商城中的一个项目,最后在一个不起眼的地方找到了这个盛世烟花。
烟花

twiik


Time and Tide wait for no man.