Edited at

Unity上でTensorFlowのCNNを動かす。

More than 1 year has passed since last update.

Unity上でTensorFlowのCNNを動かす。

この記事でTensorFLowSharpは導入済みであると仮定している.

ここを記事を見て、手持ちの環境に合わせてTensorFlowSharpをビルドしておく。


unityの導入

文法的にC#6でないと動かないと所があるので、ここからから最新版(Unity 2017.1以上)を落としておく。

https://store.unity.com/download?ref=personal


プロジェクトの作成及び準備

プロジェクトを作成後は、Assetsの下にDllsというフォルダを作り、TensorFlowSharp\TensorFlowSharp\bin\Debug の下のTensorFlowSharp.dllとSystem.ValueTuple.dllをコピーしておく。

スクリーンショット 2017-06-09 00.33.19.png

Scripting Runtime VersionをExperiental(.NET4.6 Equvalent)に変更する

スクリーンショット 2017-06-09 00.34.51.pngスクリーンショット 2017-06-09 00.35.32.png

変更後、unityの再起動する。


数字の入力用のパッドを作成する

Scriptsを作って、paint.csを作成する。

スクリーンショット 2017-06-09 00.41.00.png

スクリーンショット 2017-06-09 00.42.56.png


paint.cs

// 参考

// http://nn-hokuson.hatenablog.com/entry/2016/12/08/200133
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;

public class paint : MonoBehaviour {

Texture2D drawTexture;
Color[] buffer;

// Use this for initialization
void Start () {
Texture2D mainTexture = (Texture2D)GetComponent<Renderer>().material.mainTexture;
Color[] pixels = mainTexture.GetPixels();

buffer = new Color[pixels.Length];
pixels.CopyTo(buffer, 0);

drawTexture = new Texture2D(mainTexture.width, mainTexture.height, TextureFormat.RGBA32, false);
drawTexture.filterMode = FilterMode.Point;
}
// ブラシの太さを変える
public void Draw(Vector2 p)
{
//buffer.SetValue(Color.black, (int)p.x + 256 * (int)p.y);

//太字
for (int x = 0; x < 256; x++)
{
for (int y = 0; y < 256; y++)
{
if ((p - new Vector2(x, y)).magnitude < 5)
{
buffer.SetValue(Color.black, x + 256 * y);
}
}
}
}

// 毎フレーム、テクスチャ上のすべてのピクセルをチェックして、マウスが乗っている座標からの距離が8以下なら黒く塗りつぶします。
void Update () {
if (Input.GetMouseButton(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100.0f))
{
Draw(hit.textureCoord * 256);
}

drawTexture.SetPixels(buffer);
drawTexture.Apply();
GetComponent<Renderer>().material.mainTexture = drawTexture;
}
}
// テクスチャをjpgとして保存
public void SaveTexture()
{
byte[] data = drawTexture.EncodeToJPG();
File.WriteAllBytes(Application.dataPath + "/saveImage.jpg", data);
}
}


入力パッドとなるplaneを追加する。

スクリーンショット 2017-06-09 00.50.24.png

スクリーンショット 2017-06-09 00.50.43.png

planeの位置を以下のように変更する。

スクリーンショット 2017-06-09 00.52.54.png

Main cameraを操作する。

x:1, y:5, z:-9 がちょうどいい

スクリーンショット 2017-06-09 00.56.28.png

↓テクスチャ画像


texture.jpg


Assetsの直下にtexture.jpgを配置する。

スクリーンショット 2017-06-09 00.59.39.png

texture.jpgをplaneにドラッグアンドドロップして、アタッチする。

スクリーンショット 2017-06-09 01.01.12.png

Albedoをクリックすると、texture.jpgがアタッチされていることが確認できる

スクリーンショット 2017-06-09 01.02.00.png

Shader をStandard から Unit->Textureに変更する

スクリーンショット 2017-06-09 01.07.07.png

次にtexture.jpgを選択して、Read/Write Enabledにチェックを入れる。

スクリーンショット 2017-06-09 01.04.37.png

planeにpaint.csをアタッチする

スクリーンショット 2017-06-09 1.10.16.png

すると、planeに書き込めるようになる

スクリーンショット 2017-06-09 01.13.07.png


入力された数字画像の保存と認識をするためのボタンを作成する

次はボタンを追加する。

スクリーンショット 2017-06-09 01.13.57.png

ボタンのtextをRrecognitionにする

スクリーンショット 2017-06-09 01.15.40.png

ボタンのonclickの設定を行う。

Inspectorの on Click()の[+]を押して、リスナーのメソッドを追加する。paint.SaveTexture()を追加する。

スクリーンショット 2017-06-09 01.16.15.png

paint.csはplaneにアタッチしてあるので、planeを選択する。

スクリーンショット 2017-06-09 01.16.27.png

メソッドは paint -> SaveTexture() を選択する。

スクリーンショット 2017-06-09 01.16.44.png

そして、planeに何かを書き込み後、ボタンを押すとAssetsの直下に画像が保存される。


数字の認識のための準備を行う。

paint.csにコードを加筆する。


paint.cs

public void SaveTexture()

{
・・・
Debug.Log("Environment.CurrentDirectory");
Debug.Log("TensorFlow version: " + TFCore.Version);

var t = new SampleTest.MainClass();
t.MNSIT_read_model();
}


このような構成にして、スクリプトや学習モデルを配置する。学習モデルは、前回作成したものを使用する。

Assets

 ├── Scripts

 │   ├── DataConverter.cs

 │    ├── Datasets

 │    │    ├── Helper.cs

 │   │    └── MNIST.cs

 │   ├── SampleTest.cs

 │    └── paint.cs

 └── models

     ├── Auto_model.pb

     └── labels.txt

Assetsの直下にmodelsを作成して、中に前回の記事で作成したAuto_model.pbとlabels.txtを入れる。

TensorFlowSharpのLearnの中のDataConverter.csとDatasets/Helper.cs, Datasets/MNIST.csをScriptsの下に配置する。


Sample.cs

using System.Collections;

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using TensorFlow;
using System.IO;
using System.Collections.Generic;
using Learn.Mnist;
using System.Linq;
using UnityEngine;

namespace SampleTest
{
class MainClass
{

// Convert the image in filename to a Tensor suitable as input to the Inception model.
static TFTensor CreateTensorFromImageFile(string file)
{
var contents = File.ReadAllBytes(file);

// DecodeJpeg uses a scalar String-valued tensor as input.
var tensor = TFTensor.CreateString(contents);

TFGraph graph;
TFOutput input, output;

// Construct a graph to normalize the image
ConstructGraphToNormalizeImage(out graph, out input, out output);

// Execute that graph to normalize this one image
using (var session = new TFSession(graph))
{
var normalized = session.Run(
inputs: new[] { input },
inputValues: new[] { tensor },
outputs: new[] { output });

return normalized[0];
}
}

//開始モデルは、非常に特定の正規化されたフォーマット(特定の画像サイズ、入力テンソルの形状、正規化されたピクセル値など)
//でテンソルによって記述された画像を入力として取ります。
//このファンクションは、入力としてJPEGでエンコードされた文字列を取り込み、
//入力モデルとしての入力として適したテンソルを戻すTensorFlow操作のグラフを作成します。
static void ConstructGraphToNormalizeImage(out TFGraph graph, out TFOutput input, out TFOutput output)
{
// - モデルは28x28ピクセルにスケーリングされた画像で訓練されました。
// - モノクロなので表される色は1色のみ。(値 - 平均)/ スケールを使用してfloatに変換して使用する。
// 画素値を0-255 から 0-1 の範囲にするので、変換値 = (Mean - 画素値) / Scale の式から,
// Mean = 255, Scale = 255 となる。

const int W = 28;
const int H = 28;
const float Mean = 255;
const float Scale = 255;
const int channels = 1;

graph = new TFGraph();
input = graph.Placeholder(TFDataType.String);

output = graph.Div(
x: graph.Sub(
x: graph.Const(Mean, "mean"),
y: graph.ResizeBilinear(
images: graph.ExpandDims(
input: graph.Cast(graph.DecodeJpeg(contents: input, channels: channels), DstT: TFDataType.Float),
dim: graph.Const(0, "make_batch")),
size: graph.Const(new int[] { W, H }, "size"))),
y: graph.Const(Scale, "scale"));
}
// pythonで作成したモデルの読込を行う
public void MNSIT_read_model()
{
var graph = new TFGraph();

//var model = File.ReadAllBytes("tensorflow_inception_graph.pb");

// シリアル化されたGraphDefをファイルからロードします。
var model = File.ReadAllBytes(Application.dataPath + "/models/Auto_model.pb");
graph.Import(model, "");

using (var session = new TFSession(graph))
{
var labels = File.ReadAllLines(Application.dataPath + "/models/labels.txt");

var file = Application.dataPath + "/saveImage.jpg";

//画像ファイルに対して推論を実行する
//複数のイメージの場合、session.Run()はループで(同時に)呼び出すことができます。
//あるいは、モデルが画像データのバッチを入力として受け入れるので、画像をバッチ処理することができる。
var tensor = CreateTensorFromImageFile(file);

var runner = session.GetRunner();
// 学習モデルのグラフを指定する。
// 入出力テンソルの名前をsessionに登録する
// 手動で変換したモデルの読込のときは、.AddInput(graph["dropout"][0], 0.5f)はいらない。
runner.AddInput(graph["input"][0], tensor).AddInput(graph["dropout"][0], 0.5f).Fetch(graph["output"][0]);
var output = runner.Run();

// output[0].Value()は、「バッチ」内の各画像のラベルの確率を含むベクトルです。 バッチサイズは1であった。
//最も可能性の高いラベルインデックスを見つけます。
var result = output[0];
var rshape = result.Shape;
if (result.NumDims != 2 || rshape[0] != 1)
{
var shape = "";
foreach (var d in rshape)
{
shape += $"{d} ";
}
shape = shape.Trim();
Debug.Log($"Error: expected to produce a [1 N] shaped tensor where N is the number of labels, instead it produced one with shape [{shape}]");
Environment.Exit(1);
}

var bestIdx = 0;
float best = 0;
// 尤も確率が高いものを調べて表示する
var probabilities = ((float[][])result.GetValue(true))[0];
for (int i = 0; i < probabilities.Length; i++)
{
if (probabilities[i] > best)
{
bestIdx = i;
best = probabilities[i];
}
}
Debug.Log($"{file} best match: [{bestIdx}] {best * 100.0}% {labels[bestIdx]}");
}
}
}
}



実行する

△の90°傾けたボタンを押し、入力パッドに数字を入力後、Rrecognitionボタンを押すと、結果が表示される。

実行画面

スクリーンショット 2017-06-11 00.34.16.png

スクリーンショット 2017-06-11 00.35.30.png

プロジェクトを上げておく。

https://github.com/MaruTama/tensorflow_for_unity

参考

Unityでテクスチャにお絵描きしよう